From 047a557cc22b1c6e4463f9c7c1912c9c16b2600d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Conny=20Sj=C3=B6gren?= Date: Tue, 14 Oct 2025 08:27:31 +0200 Subject: [PATCH] refactor and fix redirecturl to bankid --- samples/Standalone.MvcSample/Program.cs | 5 +- .../Controllers/BankIdUiAuthApiController.cs | 37 +- .../Controllers/BankIdUiAuthController.cs | 2 +- .../BankIdUiPaymentApiController.cs | 42 +- .../Controllers/BankIdUiPaymentController.cs | 2 +- .../Controllers/BankIdUiSignApiController.cs | 41 +- .../Controllers/BankIdUiSignController.cs | 2 +- .../Models/BankIdUiApiInitializeResponse.cs | 10 +- .../Auth/BankIdAuthHandler.cs | 4 +- .../BankIdCommonConfiguration.cs | 5 + .../BankIdConstants.cs | 3 - .../BankIdRedirectUrlResolver.cs | 76 +++ .../Client/activelogin-main.ts | 4 +- .../IBankIdBuilderExtensions.cs | 2 + .../Launcher/BankIdCustomBrowserResolver.cs | 24 + .../Flow/BankIdFlowService.cs | 305 ++++------ .../Launcher/BankIdDevelopmentLauncher.cs | 3 +- .../Launcher/BankIdLaunchInfo.cs | 84 +-- .../Launcher/BankIdLauncher.cs | 122 +--- .../BankIdLauncherCustomBrowserByContext.cs | 20 +- .../BankIdLauncherCustomBrowserConfig.cs | 28 +- .../Launcher/BankIdRedirectUrl.cs | 156 +++++ .../Launcher/IBankIdRedirectUrlResolver.cs | 13 + .../Launcher/ICustomBrowserResolver.cs | 6 + .../Option.cs | 36 ++ .../Result.cs | 61 ++ .../ServiceCollectionBankIdExtensions.cs | 6 +- .../BankId_UiAuth_Tests.cs | 64 +- .../BankId_UiPayment_Tests.cs | 53 +- .../BankId_UiSign_Tests.cs | 53 +- .../Helpers/TestBankIdLauncher.cs | 2 +- .../BankIdLauncher_Tests.cs | 71 +-- .../BankIdRedirectUriTests.cs | 572 ++++++++++++++++++ .../BankIdTestDevices.cs | 74 +++ .../Helpers/TestBankIdLauncher.cs | 2 +- 35 files changed, 1324 insertions(+), 666 deletions(-) create mode 100644 src/ActiveLogin.Authentication.BankId.AspNetCore/BankIdRedirectUrlResolver.cs create mode 100644 src/ActiveLogin.Authentication.BankId.AspNetCore/Launcher/BankIdCustomBrowserResolver.cs create mode 100644 src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdRedirectUrl.cs create mode 100644 src/ActiveLogin.Authentication.BankId.Core/Launcher/IBankIdRedirectUrlResolver.cs create mode 100644 src/ActiveLogin.Authentication.BankId.Core/Launcher/ICustomBrowserResolver.cs create mode 100644 src/ActiveLogin.Authentication.BankId.Core/Option.cs create mode 100644 src/ActiveLogin.Authentication.BankId.Core/Result.cs create mode 100644 test/ActiveLogin.Authentication.BankId.Core.Test/BankIdRedirectUriTests.cs create mode 100644 test/ActiveLogin.Authentication.BankId.Core.Test/BankIdTestDevices.cs diff --git a/samples/Standalone.MvcSample/Program.cs b/samples/Standalone.MvcSample/Program.cs index 5efa57cb..6cbbfb3a 100644 --- a/samples/Standalone.MvcSample/Program.cs +++ b/samples/Standalone.MvcSample/Program.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.CookiePolicy; using Microsoft.AspNetCore.Localization; +using ActiveLogin.Authentication.BankId.Core.Launcher; // @@ -76,8 +77,8 @@ bankId.UseQrCoderQrCodeGenerator(); bankId.UseUaParserDeviceDetection(); - bankId.AddCustomBrowserByUserAgent(userAgent => userAgent.Contains("Instagram"), "instagram://"); - bankId.AddCustomBrowserByUserAgent(userAgent => userAgent.Contains("FBAN") || userAgent.Contains("FBAV"), "fb://"); + bankId.AddCustomBrowserByUserAgent(userAgent => userAgent.Contains("Instagram"), context => new BankIdLauncherCustomBrowserConfig(new BrowserScheme("instagram://"), BrowserMightRequireUserInteractionToLaunch.Always)); + bankId.AddCustomBrowserByUserAgent(userAgent => userAgent.Contains("FBAN") || userAgent.Contains("FBAV"), context => new BankIdLauncherCustomBrowserConfig(new BrowserScheme("fb://"), BrowserMightRequireUserInteractionToLaunch.Always)); if (configuration.GetValue("ActiveLogin:BankId:UseSimulatedEnvironment", false)) { diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiAuthApiController.cs b/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiAuthApiController.cs index 77f4201b..0ea2ff2d 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiAuthApiController.cs +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiAuthApiController.cs @@ -18,21 +18,17 @@ namespace ActiveLogin.Authentication.BankId.AspNetCore.Areas.ActiveLogin.Control [ApiController] [AllowAnonymous] [NonController] -public class BankIdUiAuthApiController : BankIdUiApiControllerBase +public class BankIdUiAuthApiController( + IBankIdFlowService bankIdFlowService, + IBankIdUiOrderRefProtector orderRefProtector, + IBankIdQrStartStateProtector qrStartStateProtector, + IBankIdUiOptionsProtector uiOptionsProtector, + IBankIdUiOptionsCookieManager uiOptionsCookieManager, + IBankIdUserMessage bankIdUserMessage, + IBankIdUserMessageLocalizer bankIdUserMessageLocalizer, + IBankIdUiResultProtector uiAuthResultProtector +) : BankIdUiApiControllerBase(bankIdFlowService, orderRefProtector, qrStartStateProtector, uiOptionsProtector, uiOptionsCookieManager, bankIdUserMessage, bankIdUserMessageLocalizer, uiAuthResultProtector) { - public BankIdUiAuthApiController( - IBankIdFlowService bankIdFlowService, - IBankIdUiOrderRefProtector orderRefProtector, - IBankIdQrStartStateProtector qrStartStateProtector, - IBankIdUiOptionsProtector uiOptionsProtector, - IBankIdUiOptionsCookieManager uiOptionsCookieManager, - IBankIdUserMessage bankIdUserMessage, - IBankIdUserMessageLocalizer bankIdUserMessageLocalizer, - IBankIdUiResultProtector uiAuthResultProtector) - : base(bankIdFlowService, orderRefProtector, qrStartStateProtector, uiOptionsProtector, uiOptionsCookieManager, bankIdUserMessage, bankIdUserMessageLocalizer, uiAuthResultProtector) - { - } - [ValidateAntiForgeryToken] [HttpPost(BankIdConstants.Routes.BankIdApiInitializeActionName)] public async Task> Initialize(BankIdUiApiInitializeRequest request) @@ -44,12 +40,7 @@ public async Task> Initialize(BankId BankIdFlowInitializeResult bankIdFlowInitializeResult; try { - var returnRedirectUrl = Url.Action(BankIdConstants.Routes.BankIdAuthInitActionName, BankIdConstants.Routes.BankIdAuthControllerName, new - { - returnUrl = request.ReturnUrl - }, protocol: Request.Scheme) ?? throw new Exception(BankIdConstants.ErrorMessages.CouldNotGetUrlFor(BankIdConstants.Routes.BankIdAuthControllerName, BankIdConstants.Routes.BankIdAuthInitActionName)); - - bankIdFlowInitializeResult = await BankIdFlowService.InitializeAuth(uiOptions.ToBankIdFlowOptions(), returnRedirectUrl); + bankIdFlowInitializeResult = await BankIdFlowService.InitializeAuth(uiOptions.ToBankIdFlowOptions(), request.ReturnUrl); } catch (BankIdApiException bankIdApiException) { @@ -65,13 +56,9 @@ public async Task> Initialize(BankId var protectedQrStartState = QrStartStateProtector.Protect(otherDevice.QrStartState); return OkJsonResult(BankIdUiApiInitializeResponse.ManualLaunch(protectedOrderRef, protectedQrStartState, otherDevice.QrCodeBase64Encoded)); } - case BankIdFlowInitializeLaunchTypeSameDevice sameDevice when sameDevice.BankIdLaunchInfo.DeviceWillReloadPageOnReturnFromBankIdApp: - { - return OkJsonResult(BankIdUiApiInitializeResponse.AutoLaunch(protectedOrderRef, sameDevice.BankIdLaunchInfo.LaunchUrl, sameDevice.BankIdLaunchInfo.DeviceMightRequireUserInteractionToLaunchBankIdApp)); - } case BankIdFlowInitializeLaunchTypeSameDevice sameDevice: { - return OkJsonResult(BankIdUiApiInitializeResponse.AutoLaunchAndCheckStatus(protectedOrderRef, sameDevice.BankIdLaunchInfo.LaunchUrl, sameDevice.BankIdLaunchInfo.DeviceMightRequireUserInteractionToLaunchBankIdApp)); + return OkJsonResult(BankIdUiApiInitializeResponse.AutoLaunchAndReloadPage(protectedOrderRef, sameDevice.BankIdLaunchInfo.LaunchUrl, sameDevice.BankIdLaunchInfo.DeviceMightRequireUserInteractionToLaunchBankIdApp)); } default: { diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiAuthController.cs b/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiAuthController.cs index b6a71053..5c719bd1 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiAuthController.cs +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiAuthController.cs @@ -33,6 +33,6 @@ IBankIdUiOptionsCookieManager uiOptionsCookieManager [Route($"/[area]/{BankIdConstants.Routes.BankIdPathName}/{BankIdConstants.Routes.BankIdAuthControllerPath}")] public Task Init(string returnUrl) { - return Initialize(returnUrl, BankIdConstants.Routes.BankIdAuthApiControllerName, "Init"); + return Initialize(returnUrl, BankIdConstants.Routes.BankIdAuthApiControllerName, nameof(Init)); } } diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiPaymentApiController.cs b/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiPaymentApiController.cs index ce4fb403..8786499a 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiPaymentApiController.cs +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiPaymentApiController.cs @@ -20,24 +20,19 @@ namespace ActiveLogin.Authentication.BankId.AspNetCore.Areas.ActiveLogin.Control [ApiController] [AllowAnonymous] [NonController] -public class BankIdUiPaymentApiController : BankIdUiApiControllerBase +public class BankIdUiPaymentApiController( + IBankIdFlowService bankIdFlowService, + IBankIdUiOrderRefProtector orderRefProtector, + IBankIdQrStartStateProtector qrStartStateProtector, + IBankIdUiOptionsProtector uiOptionsProtector, + IBankIdUiOptionsCookieManager uiOptionsCookieManager, + IBankIdUserMessage bankIdUserMessage, + IBankIdUserMessageLocalizer bankIdUserMessageLocalizer, + IBankIdUiResultProtector uiAuthResultProtector, + IBankIdUiStateProtector bankIdUiStateProtector +) : BankIdUiApiControllerBase(bankIdFlowService, orderRefProtector, qrStartStateProtector, uiOptionsProtector, uiOptionsCookieManager, bankIdUserMessage, bankIdUserMessageLocalizer, uiAuthResultProtector) { - private readonly IBankIdUiStateProtector _bankIdUiStateProtector; - - public BankIdUiPaymentApiController( - IBankIdFlowService bankIdFlowService, - IBankIdUiOrderRefProtector orderRefProtector, - IBankIdQrStartStateProtector qrStartStateProtector, - IBankIdUiOptionsProtector uiOptionsProtector, - IBankIdUiOptionsCookieManager uiOptionsCookieManager, - IBankIdUserMessage bankIdUserMessage, - IBankIdUserMessageLocalizer bankIdUserMessageLocalizer, - IBankIdUiResultProtector uiAuthResultProtector, - IBankIdUiStateProtector bankIdUiStateProtector) - : base(bankIdFlowService, orderRefProtector, qrStartStateProtector, uiOptionsProtector, uiOptionsCookieManager, bankIdUserMessage, bankIdUserMessageLocalizer, uiAuthResultProtector) - { - _bankIdUiStateProtector = bankIdUiStateProtector; - } + private readonly IBankIdUiStateProtector _bankIdUiStateProtector = bankIdUiStateProtector; [ValidateAntiForgeryToken] [HttpPost(BankIdConstants.Routes.BankIdApiInitializeActionName)] @@ -56,11 +51,6 @@ public async Task> Initialize(BankId BankIdFlowInitializeResult bankIdFlowInitializeResult; try { - var returnRedirectUrl = Url.Action(BankIdConstants.Routes.BankIdPaymentInitActionName, BankIdConstants.Routes.BankIdPaymentControllerName, new - { - returnUrl = request.ReturnUrl - }, protocol: Request.Scheme) ?? throw new Exception(BankIdConstants.ErrorMessages.CouldNotGetUrlFor(BankIdConstants.Routes.BankIdPaymentControllerName, BankIdConstants.Routes.BankIdPaymentInitActionName)); - bankIdFlowInitializeResult = await BankIdFlowService.InitializePayment( uiOptions.ToBankIdFlowOptions(), new BankIdPaymentData(state.BankIdPaymentProperties.TransactionType, state.BankIdPaymentProperties.RecipientName) @@ -78,7 +68,7 @@ public async Task> Initialize(BankId CertificatePolicies = state.BankIdPaymentProperties.BankIdCertificatePolicies, CardReader = state.BankIdPaymentProperties.CardReader, }, - returnRedirectUrl); + request.ReturnUrl); } catch (BankIdApiException bankIdApiException) { @@ -94,13 +84,9 @@ public async Task> Initialize(BankId var protectedQrStartState = QrStartStateProtector.Protect(otherDevice.QrStartState); return OkJsonResult(BankIdUiApiInitializeResponse.ManualLaunch(protectedOrderRef, protectedQrStartState, otherDevice.QrCodeBase64Encoded)); } - case BankIdFlowInitializeLaunchTypeSameDevice sameDevice when sameDevice.BankIdLaunchInfo.DeviceWillReloadPageOnReturnFromBankIdApp: - { - return OkJsonResult(BankIdUiApiInitializeResponse.AutoLaunch(protectedOrderRef, sameDevice.BankIdLaunchInfo.LaunchUrl, sameDevice.BankIdLaunchInfo.DeviceMightRequireUserInteractionToLaunchBankIdApp)); - } case BankIdFlowInitializeLaunchTypeSameDevice sameDevice: { - return OkJsonResult(BankIdUiApiInitializeResponse.AutoLaunchAndCheckStatus(protectedOrderRef, sameDevice.BankIdLaunchInfo.LaunchUrl, sameDevice.BankIdLaunchInfo.DeviceMightRequireUserInteractionToLaunchBankIdApp)); + return OkJsonResult(BankIdUiApiInitializeResponse.AutoLaunchAndReloadPage(protectedOrderRef, sameDevice.BankIdLaunchInfo.LaunchUrl, sameDevice.BankIdLaunchInfo.DeviceMightRequireUserInteractionToLaunchBankIdApp)); } default: { diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiPaymentController.cs b/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiPaymentController.cs index 5867cc20..8678ed45 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiPaymentController.cs +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiPaymentController.cs @@ -34,6 +34,6 @@ IBankIdUiOptionsCookieManager uiOptionsCookieManager [Route($"/[area]/{BankIdConstants.Routes.BankIdPathName}/{BankIdConstants.Routes.BankIdPaymentControllerPath}")] public Task Init(string returnUrl) { - return Initialize(returnUrl, BankIdConstants.Routes.BankIdPaymentApiControllerName, "Init"); + return Initialize(returnUrl, BankIdConstants.Routes.BankIdPaymentApiControllerName, nameof(Init)); } } diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiSignApiController.cs b/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiSignApiController.cs index c4789d8f..c21f5388 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiSignApiController.cs +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiSignApiController.cs @@ -20,24 +20,19 @@ namespace ActiveLogin.Authentication.BankId.AspNetCore.Areas.ActiveLogin.Control [ApiController] [AllowAnonymous] [NonController] -public class BankIdUiSignApiController : BankIdUiApiControllerBase +public class BankIdUiSignApiController( + IBankIdFlowService bankIdFlowService, + IBankIdUiOrderRefProtector orderRefProtector, + IBankIdQrStartStateProtector qrStartStateProtector, + IBankIdUiOptionsProtector uiOptionsProtector, + IBankIdUiOptionsCookieManager uiOptionsCookieManager, + IBankIdUserMessage bankIdUserMessage, + IBankIdUserMessageLocalizer bankIdUserMessageLocalizer, + IBankIdUiResultProtector uiAuthResultProtector, + IBankIdUiStateProtector bankIdUiStateProtector +) : BankIdUiApiControllerBase(bankIdFlowService, orderRefProtector, qrStartStateProtector, uiOptionsProtector, uiOptionsCookieManager, bankIdUserMessage, bankIdUserMessageLocalizer, uiAuthResultProtector) { - private readonly IBankIdUiStateProtector _bankIdUiStateProtector; - - public BankIdUiSignApiController( - IBankIdFlowService bankIdFlowService, - IBankIdUiOrderRefProtector orderRefProtector, - IBankIdQrStartStateProtector qrStartStateProtector, - IBankIdUiOptionsProtector uiOptionsProtector, - IBankIdUiOptionsCookieManager uiOptionsCookieManager, - IBankIdUserMessage bankIdUserMessage, - IBankIdUserMessageLocalizer bankIdUserMessageLocalizer, - IBankIdUiResultProtector uiAuthResultProtector, - IBankIdUiStateProtector bankIdUiStateProtector) - : base(bankIdFlowService, orderRefProtector, qrStartStateProtector, uiOptionsProtector, uiOptionsCookieManager, bankIdUserMessage, bankIdUserMessageLocalizer, uiAuthResultProtector) - { - _bankIdUiStateProtector = bankIdUiStateProtector; - } + private readonly IBankIdUiStateProtector _bankIdUiStateProtector = bankIdUiStateProtector; [ValidateAntiForgeryToken] [HttpPost(BankIdConstants.Routes.BankIdApiInitializeActionName)] @@ -56,10 +51,6 @@ public async Task> Initialize(BankId BankIdFlowInitializeResult bankIdFlowInitializeResult; try { - var returnRedirectUrl = Url.Action(BankIdConstants.Routes.BankIdSignInitActionName, BankIdConstants.Routes.BankIdSignControllerName, new - { - returnUrl = request.ReturnUrl - }, protocol: Request.Scheme) ?? throw new Exception(BankIdConstants.ErrorMessages.CouldNotGetUrlFor(BankIdConstants.Routes.BankIdSignControllerName, BankIdConstants.Routes.BankIdSignInitActionName)); bankIdFlowInitializeResult = await BankIdFlowService.InitializeSign( uiOptions.ToBankIdFlowOptions(), @@ -74,7 +65,7 @@ public async Task> Initialize(BankId CertificatePolicies = state.BankIdSignProperties.BankIdCertificatePolicies, CardReader = state.BankIdSignProperties.CardReader, }, - returnRedirectUrl); + request.ReturnUrl); } catch (BankIdApiException bankIdApiException) { @@ -90,13 +81,9 @@ public async Task> Initialize(BankId var protectedQrStartState = QrStartStateProtector.Protect(otherDevice.QrStartState); return OkJsonResult(BankIdUiApiInitializeResponse.ManualLaunch(protectedOrderRef, protectedQrStartState, otherDevice.QrCodeBase64Encoded)); } - case BankIdFlowInitializeLaunchTypeSameDevice sameDevice when sameDevice.BankIdLaunchInfo.DeviceWillReloadPageOnReturnFromBankIdApp: - { - return OkJsonResult(BankIdUiApiInitializeResponse.AutoLaunch(protectedOrderRef, sameDevice.BankIdLaunchInfo.LaunchUrl, sameDevice.BankIdLaunchInfo.DeviceMightRequireUserInteractionToLaunchBankIdApp)); - } case BankIdFlowInitializeLaunchTypeSameDevice sameDevice: { - return OkJsonResult(BankIdUiApiInitializeResponse.AutoLaunchAndCheckStatus(protectedOrderRef, sameDevice.BankIdLaunchInfo.LaunchUrl, sameDevice.BankIdLaunchInfo.DeviceMightRequireUserInteractionToLaunchBankIdApp)); + return OkJsonResult(BankIdUiApiInitializeResponse.AutoLaunchAndReloadPage(protectedOrderRef, sameDevice.BankIdLaunchInfo.LaunchUrl, sameDevice.BankIdLaunchInfo.DeviceMightRequireUserInteractionToLaunchBankIdApp)); } default: { diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiSignController.cs b/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiSignController.cs index 76d2a507..d0e77d94 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiSignController.cs +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiSignController.cs @@ -34,6 +34,6 @@ IBankIdUiOptionsCookieManager uiOptionsCookieManager [Route($"/[area]/{BankIdConstants.Routes.BankIdPathName}/{BankIdConstants.Routes.BankIdSignControllerPath}")] public Task Init(string returnUrl) { - return Initialize(returnUrl, BankIdConstants.Routes.BankIdSignApiControllerName, "Init"); + return Initialize(returnUrl, BankIdConstants.Routes.BankIdSignApiControllerName, nameof(Init)); } } diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Models/BankIdUiApiInitializeResponse.cs b/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Models/BankIdUiApiInitializeResponse.cs index 933f1687..d629289b 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Models/BankIdUiApiInitializeResponse.cs +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Models/BankIdUiApiInitializeResponse.cs @@ -7,7 +7,7 @@ internal BankIdUiApiInitializeResponse( bool deviceMightRequireUserInteractionToLaunchBankIdApp, bool checkStatus, string orderRef, - string? redirectUri, + string? launchUrl, string? qrStartState, string? qrCodeAsBase64) { @@ -15,7 +15,7 @@ internal BankIdUiApiInitializeResponse( DeviceMightRequireUserInteractionToLaunchBankIdApp = deviceMightRequireUserInteractionToLaunchBankIdApp; CheckStatus = checkStatus; OrderRef = orderRef; - RedirectUri = redirectUri; + LaunchUrl = launchUrl; QrStartState = qrStartState; QrCodeAsBase64 = qrCodeAsBase64; } @@ -25,7 +25,7 @@ internal BankIdUiApiInitializeResponse( public bool DeviceMightRequireUserInteractionToLaunchBankIdApp { get; } public bool CheckStatus { get; } public string OrderRef { get; } - public string? RedirectUri { get; } + public string? LaunchUrl { get; } public string? QrStartState { get; set; } public string? QrCodeAsBase64 { get; set; } @@ -35,9 +35,9 @@ public static BankIdUiApiInitializeResponse AutoLaunch(string orderRef, string r return new BankIdUiApiInitializeResponse(true, showLaunchButton, false, orderRef, redirectUri, null, null); } - public static BankIdUiApiInitializeResponse AutoLaunchAndCheckStatus(string orderRef, string redirectUri, bool showLaunchButton) + public static BankIdUiApiInitializeResponse AutoLaunchAndReloadPage(string orderRef, string launchUrl, bool showLaunchButton) { - return new BankIdUiApiInitializeResponse(true, showLaunchButton, true, orderRef, redirectUri, null, null); + return new BankIdUiApiInitializeResponse(true, showLaunchButton, false, orderRef, launchUrl, null, null); } public static BankIdUiApiInitializeResponse ManualLaunch(string orderRef, string qrStartState, string qrCodeAsBase64) diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/Auth/BankIdAuthHandler.cs b/src/ActiveLogin.Authentication.BankId.AspNetCore/Auth/BankIdAuthHandler.cs index 54ec3133..da4ecb97 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/Auth/BankIdAuthHandler.cs +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/Auth/BankIdAuthHandler.cs @@ -176,11 +176,11 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop var detectedDevice = _bankIdSupportedDeviceDetector.Detect(); await _bankIdEventTrigger.TriggerAsync(new BankIdAspNetChallengeSuccessEvent(detectedDevice, uiOptions.ToBankIdFlowOptions())); - var loginUrl = GetInitUiUrl(uiOptions); + var loginUrl = GetInitUiUrl(); Response.Redirect(loginUrl); } - private string GetInitUiUrl(BankIdUiOptions uiOptions) + private string GetInitUiUrl() { var pathBase = Context.Request.PathBase; var authUrl = pathBase.Add(_authPath); diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/BankIdCommonConfiguration.cs b/src/ActiveLogin.Authentication.BankId.AspNetCore/BankIdCommonConfiguration.cs index 036039e4..fdab961e 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/BankIdCommonConfiguration.cs +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/BankIdCommonConfiguration.cs @@ -2,11 +2,13 @@ using ActiveLogin.Authentication.BankId.AspNetCore.Cookies; using ActiveLogin.Authentication.BankId.AspNetCore.DataProtection; +using ActiveLogin.Authentication.BankId.AspNetCore.Launcher; using ActiveLogin.Authentication.BankId.AspNetCore.StateHandling; using ActiveLogin.Authentication.BankId.AspNetCore.SupportedDevice; using ActiveLogin.Authentication.BankId.AspNetCore.UserContext; using ActiveLogin.Authentication.BankId.AspNetCore.UserContext.Device; using ActiveLogin.Authentication.BankId.Core; +using ActiveLogin.Authentication.BankId.Core.Launcher; using ActiveLogin.Authentication.BankId.Core.SupportedDevice; using ActiveLogin.Authentication.BankId.Core.UserContext; @@ -30,6 +32,9 @@ public static void AddDefaultServices(IServiceCollection services) services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddHttpContextAccessor(); services.AddTransient(); diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/BankIdConstants.cs b/src/ActiveLogin.Authentication.BankId.AspNetCore/BankIdConstants.cs index 810165c6..80577ad1 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/BankIdConstants.cs +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/BankIdConstants.cs @@ -71,15 +71,12 @@ public static class Routes public const string BankIdAuthControllerName = "BankIdUiAuth"; public const string BankIdAuthControllerPath = "Auth"; - public const string BankIdAuthInitActionName = "Init"; public const string BankIdSignControllerName = "BankIdUiSign"; public const string BankIdSignControllerPath = "Sign"; - public const string BankIdSignInitActionName = "Init"; public const string BankIdPaymentControllerName = "BankIdUiPayment"; public const string BankIdPaymentControllerPath = "Payment"; - public const string BankIdPaymentInitActionName = "Init"; public const string BankIdAuthApiControllerName = "BankIdUiAuthApi"; public const string BankIdSignApiControllerName = "BankIdUiSignApi"; diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/BankIdRedirectUrlResolver.cs b/src/ActiveLogin.Authentication.BankId.AspNetCore/BankIdRedirectUrlResolver.cs new file mode 100644 index 00000000..c8613bb0 --- /dev/null +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/BankIdRedirectUrlResolver.cs @@ -0,0 +1,76 @@ +using ActiveLogin.Authentication.BankId.Core.Launcher; +using ActiveLogin.Authentication.BankId.Core.SupportedDevice; + +using Microsoft.AspNetCore.Http; + +namespace ActiveLogin.Authentication.BankId.AspNetCore; + +public class BankIdRedirectUrlResolver( + IBankIdSupportedDeviceDetector deviceDetector, + ICustomBrowserResolver customBrowserResolver, + IHttpContextAccessor httpContextAccessor +) : IBankIdRedirectUrlResolver +{ + public async Task GetRedirectUrl(BankIdTransactionType type, string callbackPath) + { + var context = httpContextAccessor.HttpContext ?? throw new InvalidOperationException(BankIdConstants.ErrorMessages.CouldNotAccessHttpContext); + var controller = type switch + { + BankIdTransactionType.Auth => BankIdConstants.Routes.BankIdAuthControllerName, + BankIdTransactionType.Sign => BankIdConstants.Routes.BankIdSignControllerName, + BankIdTransactionType.Payment => BankIdConstants.Routes.BankIdPaymentControllerName, + _ => throw new InvalidOperationException("Unknown BankIdTransactionType") + }; + var returnRedirectUrl = context.ResolveControllerUrl( + BankIdConstants.Routes.ActiveLoginAreaName, + controller, + "Init", new { returnUrl = callbackPath } + ); + + var config = await customBrowserResolver.GetConfig(new LaunchUrlRequest(returnRedirectUrl, "")); + + var result = BankIdRedirectUrl.TryCreate( + returnRedirectUrl, + config, + deviceDetector.Detect() + ); + + return result switch + { + Result.Success(var value) => value, + Result.Failure(var error) => throw new InvalidOperationException($"Failed to create redirect URL: {error}"), + _ => throw new InvalidOperationException("Unexpected result when creating redirect URL") + }; + } +} + +public static class UrlHelperExtensions +{ + public static string ResolveControllerUrl( + this HttpContext context, + string area, + string controller, + string action, + object? routeValues = null + ){ + var host = context.Request.Host.Value; + var url = $"https://{host}/{area}/{controller}/{action}"; + + if (routeValues != null) + { + var properties = routeValues.GetType().GetProperties(); + var query = string.Join("&", properties.Select(p => + { + var value = p.GetValue(routeValues); + return $"{Uri.EscapeDataString(p.Name)}={Uri.EscapeDataString(value?.ToString() ?? string.Empty)}"; + })); + + if (!string.IsNullOrEmpty(query)) + { + url += "?" + query; + } + } + + return url; + } +} diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/Client/activelogin-main.ts b/src/ActiveLogin.Authentication.BankId.AspNetCore/Client/activelogin-main.ts index 9353f876..fe78a8c1 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/Client/activelogin-main.ts +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/Client/activelogin-main.ts @@ -132,7 +132,7 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: if (data.deviceMightRequireUserInteractionToLaunchBankIdApp) { var startBankIdAppButtonOnClick = (event: Event) => { - window.location.href = data.redirectUri; + window.location.href = data.launchUrl; hide(startBankIdAppButtonElement); event.target.removeEventListener("click", startBankIdAppButtonOnClick); }; @@ -140,7 +140,7 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: show(startBankIdAppButtonElement); } else { - window.location.href = data.redirectUri; + window.location.href = data.launchUrl; } } diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/IBankIdBuilderExtensions.cs b/src/ActiveLogin.Authentication.BankId.AspNetCore/IBankIdBuilderExtensions.cs index ab5d8094..c63f3b63 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/IBankIdBuilderExtensions.cs +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/IBankIdBuilderExtensions.cs @@ -16,6 +16,7 @@ public static class IBankIdBuilderExtensions /// /// /// + [Obsolete("Use AddCustomBrowserByUserAgent that returns a BankIdLauncherCustomBrowserConfig instead.")] public static IBankIdBuilder AddCustomBrowserByUserAgent(this IBankIdBuilder builder, Func isApplicable, string returnUrl) { return AddCustomBrowserByUserAgent(builder, isApplicable, context => returnUrl); @@ -29,6 +30,7 @@ public static IBankIdBuilder AddCustomBrowserByUserAgent(this IBankIdBuilder bui /// /// /// + [Obsolete("Use AddCustomBrowserByUserAgent that returns a BankIdLauncherCustomBrowserConfig instead.")] public static IBankIdBuilder AddCustomBrowserByUserAgent(this IBankIdBuilder builder, Func isApplicable, Func getReturnUrl) { BankIdLauncherCustomBrowserConfig GetResult(BankIdLauncherCustomBrowserContext context) => new(getReturnUrl(context), BrowserReloadBehaviourOnReturnFromBankIdApp.Never); diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/Launcher/BankIdCustomBrowserResolver.cs b/src/ActiveLogin.Authentication.BankId.AspNetCore/Launcher/BankIdCustomBrowserResolver.cs new file mode 100644 index 00000000..e24d4b06 --- /dev/null +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/Launcher/BankIdCustomBrowserResolver.cs @@ -0,0 +1,24 @@ +using ActiveLogin.Authentication.BankId.Core.Launcher; +using ActiveLogin.Authentication.BankId.Core.SupportedDevice; + +namespace ActiveLogin.Authentication.BankId.AspNetCore.Launcher; + +public class BankIdCustomBrowserResolver( + IBankIdSupportedDeviceDetector deviceDetector, + IEnumerable customBrowsers +) : ICustomBrowserResolver +{ + public async Task GetConfig(LaunchUrlRequest launchUrlRequest) + { + var device = deviceDetector.Detect(); + var context = new BankIdLauncherCustomBrowserContext(device, launchUrlRequest); + foreach (var browser in customBrowsers) + { + if (await browser.IsApplicable(context)) + { + return await browser.GetCustomAppCallbackResult(context); + } + } + return null; + } +} diff --git a/src/ActiveLogin.Authentication.BankId.Core/Flow/BankIdFlowService.cs b/src/ActiveLogin.Authentication.BankId.Core/Flow/BankIdFlowService.cs index a7a92b6f..a0bb5e9d 100644 --- a/src/ActiveLogin.Authentication.BankId.Core/Flow/BankIdFlowService.cs +++ b/src/ActiveLogin.Authentication.BankId.Core/Flow/BankIdFlowService.cs @@ -6,7 +6,6 @@ using ActiveLogin.Authentication.BankId.Core.Events.Infrastructure; using ActiveLogin.Authentication.BankId.Core.Launcher; using ActiveLogin.Authentication.BankId.Core.Models; -using ActiveLogin.Authentication.BankId.Core.Payment; using ActiveLogin.Authentication.BankId.Core.Qr; using ActiveLogin.Authentication.BankId.Core.Requirements; using ActiveLogin.Authentication.BankId.Core.SupportedDevice; @@ -17,73 +16,41 @@ namespace ActiveLogin.Authentication.BankId.Core.Flow; -public class BankIdFlowService : IBankIdFlowService +public class BankIdFlowService( + IBankIdAppApiClient bankIdAppApiClient, + IBankIdFlowSystemClock bankIdFlowSystemClock, + IBankIdEventTrigger bankIdEventTrigger, + IBankIdUserMessage bankIdUserMessage, + IBankIdUserMessageLocalizer bankIdUserMessageLocalizer, + IBankIdSupportedDeviceDetector bankIdSupportedDeviceDetector, + IBankIdEndUserIpResolver bankIdEndUserIpResolver, + IBankIdAuthRequestUserDataResolver bankIdAuthUserDataResolver, + IBankIdAuthRequestRequirementsResolver bankIdAuthRequestRequirementsResolver, + IBankIdQrCodeGenerator bankIdQrCodeGenerator, + IBankIdLauncher bankIdLauncher, + IBankIdCertificatePolicyResolver bankIdCertificatePolicyResolver, + IBankIdEndUserDeviceDataResolverFactory bankIdEndUserDeviceDataResolverFactory, + IBankIdRedirectUrlResolver bankIdRedirectUrlResolver +) : IBankIdFlowService { private const int MaxRetryLoginAttempts = 5; - private readonly IBankIdAppApiClient _bankIdAppApiClient; - private readonly IBankIdFlowSystemClock _bankIdFlowSystemClock; - private readonly IBankIdEventTrigger _bankIdEventTrigger; - private readonly IBankIdUserMessage _bankIdUserMessage; - private readonly IBankIdUserMessageLocalizer _bankIdUserMessageLocalizer; - private readonly IBankIdSupportedDeviceDetector _bankIdSupportedDeviceDetector; - private readonly IBankIdEndUserIpResolver _bankIdEndUserIpResolver; - private readonly IBankIdAuthRequestUserDataResolver _bankIdAuthUserDataResolver; - private readonly IBankIdAuthRequestRequirementsResolver _bankIdAuthRequestRequirementsResolver; - private readonly IBankIdQrCodeGenerator _bankIdQrCodeGenerator; - private readonly IBankIdLauncher _bankIdLauncher; - private readonly IBankIdCertificatePolicyResolver _bankIdCertificatePolicyResolver; - private readonly IBankIdEndUserDeviceDataResolverFactory _bankIdEndUserDeviceDataResolverFactory; - - public BankIdFlowService( - IBankIdAppApiClient bankIdAppApiClient, - IBankIdFlowSystemClock bankIdFlowSystemClock, - IBankIdEventTrigger bankIdEventTrigger, - IBankIdUserMessage bankIdUserMessage, - IBankIdUserMessageLocalizer bankIdUserMessageLocalizer, - IBankIdSupportedDeviceDetector bankIdSupportedDeviceDetector, - IBankIdEndUserIpResolver bankIdEndUserIpResolver, - IBankIdAuthRequestUserDataResolver bankIdAuthUserDataResolver, - IBankIdAuthRequestRequirementsResolver bankIdAuthRequestRequirementsResolver, - IBankIdQrCodeGenerator bankIdQrCodeGenerator, - IBankIdLauncher bankIdLauncher, - IBankIdCertificatePolicyResolver bankIdCertificatePolicyResolver, - IBankIdEndUserDeviceDataResolverFactory bankIdEndUserDeviceDataResolverFactory - ) + public async Task InitializeAuth( + BankIdFlowOptions flowOptions, + string authHandlerCallbackPath) { - _bankIdAppApiClient = bankIdAppApiClient; - _bankIdFlowSystemClock = bankIdFlowSystemClock; - _bankIdEventTrigger = bankIdEventTrigger; - _bankIdUserMessage = bankIdUserMessage; - _bankIdUserMessageLocalizer = bankIdUserMessageLocalizer; - _bankIdSupportedDeviceDetector = bankIdSupportedDeviceDetector; - _bankIdEndUserIpResolver = bankIdEndUserIpResolver; - _bankIdAuthUserDataResolver = bankIdAuthUserDataResolver; - _bankIdAuthRequestRequirementsResolver = bankIdAuthRequestRequirementsResolver; - _bankIdQrCodeGenerator = bankIdQrCodeGenerator; - _bankIdLauncher = bankIdLauncher; - _bankIdCertificatePolicyResolver = bankIdCertificatePolicyResolver; - _bankIdEndUserDeviceDataResolverFactory = bankIdEndUserDeviceDataResolverFactory; - } - - public async Task InitializeAuth(BankIdFlowOptions flowOptions, string returnRedirectUrl) - { - var detectedUserDevice = _bankIdSupportedDeviceDetector.Detect(); - var returnUrl = await GetReturnUrl(flowOptions, returnRedirectUrl); - - var response = await AuthAsync(flowOptions, detectedUserDevice, returnUrl); - - await _bankIdEventTrigger.TriggerAsync(new BankIdInitializeSuccessEvent(personalIdentityNumber: null, response.OrderRef, detectedUserDevice, flowOptions)); + var detectedUserDevice = bankIdSupportedDeviceDetector.Detect(); + var response = await AuthAsync(flowOptions, detectedUserDevice, authHandlerCallbackPath); if (flowOptions.SameDevice) { - var launchInfo = await GetBankIdLaunchInfo(returnRedirectUrl, response.AutoStartToken); + var launchInfo = await bankIdLauncher.GetLaunchInfoAsync(new LaunchUrlRequest(authHandlerCallbackPath, response.AutoStartToken)); return new BankIdFlowInitializeResult(response, detectedUserDevice, new BankIdFlowInitializeLaunchTypeSameDevice(launchInfo)); } else { var qrStartState = new BankIdQrStartState( - _bankIdFlowSystemClock.UtcNow, + bankIdFlowSystemClock.UtcNow, response.QrStartToken, response.QrStartSecret ); @@ -93,70 +60,72 @@ public async Task InitializeAuth(BankIdFlowOptions f } } - private async Task AuthAsync(BankIdFlowOptions flowOptions, BankIdSupportedDevice detectedUserDevice, string? returnUrl) + private async Task AuthAsync( + BankIdFlowOptions flowOptions, + BankIdSupportedDevice detectedUserDevice, + string authHandlerCallbackPath) { try { - var request = await GetAuthRequest(flowOptions, returnUrl); - return await _bankIdAppApiClient.AuthAsync(request); + var request = await GetAuthRequest(flowOptions, authHandlerCallbackPath); + var response = await bankIdAppApiClient.AuthAsync(request); + await bankIdEventTrigger.TriggerAsync(new BankIdInitializeSuccessEvent(personalIdentityNumber: null, response.OrderRef, detectedUserDevice, flowOptions)); + return response; } catch (BankIdApiException bankIdApiException) { - await _bankIdEventTrigger.TriggerAsync(new BankIdInitializeErrorEvent(personalIdentityNumber: null, bankIdApiException, detectedUserDevice, flowOptions)); + await bankIdEventTrigger.TriggerAsync(new BankIdInitializeErrorEvent(personalIdentityNumber: null, bankIdApiException, detectedUserDevice, flowOptions)); throw; } } - private async Task GetAuthRequest(BankIdFlowOptions flowOptions, string? returnUrl) + private async Task GetAuthRequest(BankIdFlowOptions flowOptions, string authHandlerCallbackPath) { - var endUserIp = _bankIdEndUserIpResolver.GetEndUserIp(); - var resolvedRequirements = await _bankIdAuthRequestRequirementsResolver.GetRequirementsAsync(); + var resolvedRequirements = await bankIdAuthRequestRequirementsResolver.GetRequirementsAsync(); var requiredPersonalIdentityNumber = resolvedRequirements.RequiredPersonalIdentityNumber ?? flowOptions.RequiredPersonalIdentityNumber; - var requireMrtd = resolvedRequirements.RequireMrtd ?? flowOptions.RequireMrtd; - var requirePinCode = resolvedRequirements.RequirePinCode ?? flowOptions.RequirePinCode; - var certificatePolicies = resolvedRequirements.CertificatePolicies.Any() ? resolvedRequirements.CertificatePolicies : flowOptions.CertificatePolicies; - var resolvedCertificatePolicies = GetResolvedCertificatePolicies(certificatePolicies, flowOptions.SameDevice); - - var cardReader = resolvedRequirements.CardReader ?? flowOptions.CardReader; - var requestRequirement = new Requirement(resolvedCertificatePolicies, requirePinCode, requireMrtd, requiredPersonalIdentityNumber?.To12DigitString(), cardReader); + var certificatePolicies = resolvedRequirements.CertificatePolicies.Any() + ? resolvedRequirements.CertificatePolicies + : flowOptions.CertificatePolicies; - var returnRisk = flowOptions.ReturnRisk; + var requestRequirement = new Requirement( + GetResolvedCertificatePolicies(certificatePolicies, flowOptions.SameDevice), + resolvedRequirements.RequirePinCode ?? flowOptions.RequirePinCode, + resolvedRequirements.RequireMrtd ?? flowOptions.RequireMrtd, + requiredPersonalIdentityNumber?.To12DigitString(), + resolvedRequirements.CardReader ?? flowOptions.CardReader + ); - var authRequestContext = new BankIdAuthRequestContext(endUserIp, requestRequirement); - var userData = await _bankIdAuthUserDataResolver.GetUserDataAsync(authRequestContext); + var authRequestContext = new BankIdAuthRequestContext(bankIdEndUserIpResolver.GetEndUserIp(), requestRequirement); + var userData = await bankIdAuthUserDataResolver.GetUserDataAsync(authRequestContext); var (webDeviceData, appDeviceData) = GetDeviceData(); return new AuthRequest( - endUserIp, + bankIdEndUserIpResolver.GetEndUserIp(), requirement: requestRequirement, userVisibleData: userData.UserVisibleData, userNonVisibleData: userData.UserNonVisibleData, userVisibleDataFormat: userData.UserVisibleDataFormat, - returnUrl: returnUrl, - returnRisk: returnRisk, + returnUrl: await bankIdRedirectUrlResolver.GetRedirectUrl(BankIdTransactionType.Auth, authHandlerCallbackPath), + returnRisk: flowOptions.ReturnRisk, web: webDeviceData, app: appDeviceData ); } - public async Task InitializeSign(BankIdFlowOptions flowOptions, BankIdSignData bankIdSignData, string returnRedirectUrl) + public async Task InitializeSign(BankIdFlowOptions flowOptions, BankIdSignData bankIdSignData, string callbackPath) { - var detectedUserDevice = _bankIdSupportedDeviceDetector.Detect(); - var returnUrl = await GetReturnUrl(flowOptions, returnRedirectUrl); - - var response = await SignAsync(flowOptions, bankIdSignData, detectedUserDevice, returnUrl); - - await _bankIdEventTrigger.TriggerAsync(new BankIdInitializeSuccessEvent(personalIdentityNumber: null, response.OrderRef, detectedUserDevice, flowOptions)); + var detectedUserDevice = bankIdSupportedDeviceDetector.Detect(); + var response = await SignAsync(flowOptions, bankIdSignData, detectedUserDevice, callbackPath); if (flowOptions.SameDevice) { - var launchInfo = await GetBankIdLaunchInfo(returnRedirectUrl, response.AutoStartToken); + var launchInfo = await bankIdLauncher.GetLaunchInfoAsync(new LaunchUrlRequest(callbackPath, response.AutoStartToken)); return new BankIdFlowInitializeResult(response, detectedUserDevice, new BankIdFlowInitializeLaunchTypeSameDevice(launchInfo)); } else { var qrStartState = new BankIdQrStartState( - _bankIdFlowSystemClock.UtcNow, + bankIdFlowSystemClock.UtcNow, response.QrStartToken, response.QrStartSecret ); @@ -166,47 +135,48 @@ public async Task InitializeSign(BankIdFlowOptions f } } - private async Task SignAsync(BankIdFlowOptions flowOptions, BankIdSignData bankIdSignData, BankIdSupportedDevice detectedUserDevice, string? returnUrl) + private async Task SignAsync(BankIdFlowOptions flowOptions, BankIdSignData bankIdSignData, BankIdSupportedDevice detectedUserDevice, string callbackPath) { try { - var request = GetSignRequest(flowOptions, bankIdSignData, returnUrl); - return await _bankIdAppApiClient.SignAsync(request); + var request = await GetSignRequest(flowOptions, bankIdSignData, callbackPath); + var response = await bankIdAppApiClient.SignAsync(request); + await bankIdEventTrigger.TriggerAsync(new BankIdInitializeSuccessEvent(personalIdentityNumber: null, response.OrderRef, detectedUserDevice, flowOptions)); + return response; } catch (BankIdApiException bankIdApiException) { - await _bankIdEventTrigger.TriggerAsync(new BankIdInitializeErrorEvent(personalIdentityNumber: null, bankIdApiException, detectedUserDevice, flowOptions)); + await bankIdEventTrigger.TriggerAsync(new BankIdInitializeErrorEvent(personalIdentityNumber: null, bankIdApiException, detectedUserDevice, flowOptions)); throw; } } - private SignRequest GetSignRequest(BankIdFlowOptions flowOptions, BankIdSignData bankIdSignData, string? returnUrl) + private async Task GetSignRequest(BankIdFlowOptions flowOptions, BankIdSignData bankIdSignData, string callbackPath) { - var endUserIp = _bankIdEndUserIpResolver.GetEndUserIp(); - var certificatePolicies = bankIdSignData.CertificatePolicies.Any() ? bankIdSignData.CertificatePolicies : flowOptions.CertificatePolicies; - var resolvedCertificatePolicies = GetResolvedCertificatePolicies(certificatePolicies, flowOptions.SameDevice); var requiredPersonalIdentityNumber = bankIdSignData.RequiredPersonalIdentityNumber ?? flowOptions.RequiredPersonalIdentityNumber; - var requireMrtd = bankIdSignData.RequireMrtd ?? flowOptions.RequireMrtd; - var requirePinCode = bankIdSignData.RequirePinCode ?? flowOptions.RequirePinCode; - var cardReader = bankIdSignData.CardReader ?? flowOptions.CardReader; - var requestRequirement = new Requirement(resolvedCertificatePolicies, requirePinCode, requireMrtd, requiredPersonalIdentityNumber?.To12DigitString(), cardReader); - var returnRisk = flowOptions.ReturnRisk; + var requestRequirement = new Requirement( + GetResolvedCertificatePolicies(certificatePolicies, flowOptions.SameDevice), + bankIdSignData.RequirePinCode ?? flowOptions.RequirePinCode, + bankIdSignData.RequireMrtd ?? flowOptions.RequireMrtd, + requiredPersonalIdentityNumber?.To12DigitString(), + bankIdSignData.CardReader ?? flowOptions.CardReader + ); var (webDeviceData, appDeviceData) = GetDeviceData(); return new SignRequest( - endUserIp, + bankIdEndUserIpResolver.GetEndUserIp(), bankIdSignData.UserVisibleData, userNonVisibleData: bankIdSignData.UserNonVisibleData, userVisibleDataFormat: bankIdSignData.UserVisibleDataFormat, requirement: requestRequirement, - returnUrl: returnUrl, - returnRisk: returnRisk, + returnUrl: await bankIdRedirectUrlResolver.GetRedirectUrl(BankIdTransactionType.Sign, callbackPath), + returnRisk: flowOptions.ReturnRisk, web: webDeviceData, app: appDeviceData ); @@ -214,7 +184,7 @@ private SignRequest GetSignRequest(BankIdFlowOptions flowOptions, BankIdSignData private (DeviceDataWeb?, DeviceDataApp?) GetDeviceData() { - var deviceDataResolver = _bankIdEndUserDeviceDataResolverFactory.GetResolver(); + var deviceDataResolver = bankIdEndUserDeviceDataResolverFactory.GetResolver(); var deviceData = deviceDataResolver.GetDeviceData(); return deviceDataResolver.DeviceType switch { @@ -224,24 +194,20 @@ private SignRequest GetSignRequest(BankIdFlowOptions flowOptions, BankIdSignData }; } - public async Task InitializePayment(BankIdFlowOptions flowOptions, BankIdPaymentData bankIdPaymentData, string returnRedirectUrl) + public async Task InitializePayment(BankIdFlowOptions flowOptions, BankIdPaymentData bankIdPaymentData, string controllerInitUrl) { - var detectedUserDevice = _bankIdSupportedDeviceDetector.Detect(); - var returnUrl = await GetReturnUrl(flowOptions, returnRedirectUrl); - - var response = await PaymentAsync(flowOptions, bankIdPaymentData, detectedUserDevice, returnUrl); - - await _bankIdEventTrigger.TriggerAsync(new BankIdInitializeSuccessEvent(personalIdentityNumber: null, response.OrderRef, detectedUserDevice, flowOptions)); + var detectedUserDevice = bankIdSupportedDeviceDetector.Detect(); + var response = await PaymentAsync(flowOptions, bankIdPaymentData, detectedUserDevice, controllerInitUrl); if (flowOptions.SameDevice) { - var launchInfo = await GetBankIdLaunchInfo(returnRedirectUrl, response.AutoStartToken); + var launchInfo = await bankIdLauncher.GetLaunchInfoAsync(new LaunchUrlRequest(controllerInitUrl, response.AutoStartToken)); return new BankIdFlowInitializeResult(response, detectedUserDevice, new BankIdFlowInitializeLaunchTypeSameDevice(launchInfo)); } else { var qrStartState = new BankIdQrStartState( - _bankIdFlowSystemClock.UtcNow, + bankIdFlowSystemClock.UtcNow, response.QrStartToken, response.QrStartSecret ); @@ -251,55 +217,62 @@ public async Task InitializePayment(BankIdFlowOption } } - private async Task PaymentAsync(BankIdFlowOptions flowOptions, BankIdPaymentData bankIdPaymentData, BankIdSupportedDevice detectedUserDevice, string? returnUrl) + private async Task PaymentAsync(BankIdFlowOptions flowOptions, BankIdPaymentData bankIdPaymentData, BankIdSupportedDevice detectedUserDevice, string callbackPath) { try { - var request = GetPaymentRequest(flowOptions, bankIdPaymentData, returnUrl); - return await _bankIdAppApiClient.PaymentAsync(request); + var request = await GetPaymentRequest(flowOptions, bankIdPaymentData, callbackPath); + var response = await bankIdAppApiClient.PaymentAsync(request); + await bankIdEventTrigger.TriggerAsync(new BankIdInitializeSuccessEvent(personalIdentityNumber: null, response.OrderRef, detectedUserDevice, flowOptions)); + return response; } catch (BankIdApiException bankIdApiException) { - await _bankIdEventTrigger.TriggerAsync(new BankIdInitializeErrorEvent(personalIdentityNumber: null, bankIdApiException, detectedUserDevice, flowOptions)); + await bankIdEventTrigger.TriggerAsync(new BankIdInitializeErrorEvent(personalIdentityNumber: null, bankIdApiException, detectedUserDevice, flowOptions)); throw; } } - private PaymentRequest GetPaymentRequest(BankIdFlowOptions flowOptions, BankIdPaymentData bankIdPaymentData, string? returnUrl) + private async Task GetPaymentRequest(BankIdFlowOptions flowOptions, BankIdPaymentData bankIdPaymentData, string callbackPath) { - var endUserIp = _bankIdEndUserIpResolver.GetEndUserIp(); - - var transactionType = bankIdPaymentData.TransactionType; - var recipientName = bankIdPaymentData.RecipientName; - var recipient = new Recipient(recipientName); - var money = bankIdPaymentData.Money; - var riskWarning = bankIdPaymentData.RiskWarning; - var userVisibleTransaction = new UserVisibleTransaction(transactionType.ToString(), recipient, money, riskWarning); + var userVisibleTransaction = new UserVisibleTransaction( + bankIdPaymentData.TransactionType.ToString(), + new Recipient(bankIdPaymentData.RecipientName), + bankIdPaymentData.Money, + bankIdPaymentData.RiskWarning + ); - var certificatePolicies = bankIdPaymentData.CertificatePolicies.Any() ? bankIdPaymentData.CertificatePolicies : flowOptions.CertificatePolicies; - var resolvedCertificatePolicies = GetResolvedCertificatePolicies(certificatePolicies, flowOptions.SameDevice); - var cardReader = bankIdPaymentData.CardReader ?? flowOptions.CardReader; + var certificatePolicies = bankIdPaymentData.CertificatePolicies.Any() + ? bankIdPaymentData.CertificatePolicies + : flowOptions.CertificatePolicies; var requiredPersonalIdentityNumber = bankIdPaymentData.RequiredPersonalIdentityNumber ?? flowOptions.RequiredPersonalIdentityNumber; - var requireMrtd = bankIdPaymentData.RequireMrtd ?? flowOptions.RequireMrtd; - var requirePinCode = bankIdPaymentData.RequirePinCode ?? flowOptions.RequirePinCode; - var requestRequirement = new Requirement(resolvedCertificatePolicies, requirePinCode, requireMrtd, requiredPersonalIdentityNumber?.To12DigitString(), cardReader); - var returnRisk = bankIdPaymentData.ReturnRisk ?? flowOptions.ReturnRisk; - - var riskFlags = GetResolvedRiskFlags(bankIdPaymentData.RiskFlags); + var requestRequirement = new Requirement( + GetResolvedCertificatePolicies(certificatePolicies, flowOptions.SameDevice), + bankIdPaymentData.RequirePinCode ?? flowOptions.RequirePinCode, + bankIdPaymentData.RequireMrtd ?? flowOptions.RequireMrtd, + requiredPersonalIdentityNumber?.To12DigitString(), + bankIdPaymentData.CardReader ?? flowOptions.CardReader + ); var (webDeviceData, appDeviceData) = GetDeviceData(); + var riskFlags = bankIdPaymentData.RiskFlags == null || !bankIdPaymentData.RiskFlags.Any() + ? null + : bankIdPaymentData.RiskFlags + .Select(x => x.ToString()) + .ToList(); + return new PaymentRequest( - endUserIp, + bankIdEndUserIpResolver.GetEndUserIp(), userVisibleTransaction, userVisibleData: bankIdPaymentData.UserVisibleData, userNonVisibleData: bankIdPaymentData.UserNonVisibleData, userVisibleDataFormat: bankIdPaymentData.UserVisibleDataFormat, requirement: requestRequirement, - returnRisk: returnRisk, - returnUrl: returnUrl, + returnRisk: bankIdPaymentData.ReturnRisk ?? flowOptions.ReturnRisk, + returnUrl: await bankIdRedirectUrlResolver.GetRedirectUrl(BankIdTransactionType.Payment, callbackPath), riskFlags: riskFlags, app: appDeviceData, web: webDeviceData @@ -321,36 +294,14 @@ private PaymentRequest GetPaymentRequest(BankIdFlowOptions flowOptions, BankIdPa } } - return certificatePolicies.Select(x => _bankIdCertificatePolicyResolver.Resolve(x)).ToList(); - } - - private List? GetResolvedRiskFlags(IEnumerable? riskFlags) - { - if (riskFlags == null || !riskFlags.Any()) return null; - - return riskFlags.Select(x => x.ToString()).ToList(); - } - - private async Task GetReturnUrl(BankIdFlowOptions flowOptions, string returnRedirectUrl) - { - if (!flowOptions.SameDevice) return null; - - var launchUrlRequest = new LaunchUrlRequest(returnRedirectUrl, ""); - var launchInfo = await _bankIdLauncher.GetLaunchInfoAsync(launchUrlRequest); - - return launchInfo.ReturnUrl; - } - - private Task GetBankIdLaunchInfo(string redirectUrl, string autoStartToken) - { - var launchUrlRequest = new LaunchUrlRequest(redirectUrl, autoStartToken); - - return _bankIdLauncher.GetLaunchInfoAsync(launchUrlRequest); + return certificatePolicies + .Select(bankIdCertificatePolicyResolver.Resolve) + .ToList(); } public async Task Collect(string orderRef, int autoStartAttempts, BankIdFlowOptions flowOptions) { - var detectedUserDevice = _bankIdSupportedDeviceDetector.Detect(); + var detectedUserDevice = bankIdSupportedDeviceDetector.Detect(); var collectResponse = await GetCollectResponse(orderRef, flowOptions, detectedUserDevice); var statusMessage = GetStatusMessage(collectResponse, flowOptions, detectedUserDevice); @@ -360,7 +311,7 @@ public async Task Collect(string orderRef, int autoStar { case CollectStatus.Pending: { - await _bankIdEventTrigger.TriggerAsync(new BankIdCollectPendingEvent(collectResponse.OrderRef, collectResponse.GetCollectHintCode(), detectedUserDevice, flowOptions)); + await bankIdEventTrigger.TriggerAsync(new BankIdCollectPendingEvent(collectResponse.OrderRef, collectResponse.GetCollectHintCode(), detectedUserDevice, flowOptions)); return new BankIdFlowCollectResultPending(statusMessage); } case CollectStatus.Complete: @@ -370,7 +321,7 @@ public async Task Collect(string orderRef, int autoStar throw new InvalidOperationException("Missing CompletionData from BankID API"); } - await _bankIdEventTrigger.TriggerAsync(new BankIdCollectCompletedEvent(collectResponse.OrderRef, collectResponse.CompletionData, detectedUserDevice, flowOptions)); + await bankIdEventTrigger.TriggerAsync(new BankIdCollectCompletedEvent(collectResponse.OrderRef, collectResponse.CompletionData, detectedUserDevice, flowOptions)); return new BankIdFlowCollectResultComplete(collectResponse.CompletionData); } case CollectStatus.Failed: @@ -381,12 +332,12 @@ public async Task Collect(string orderRef, int autoStar return new BankIdFlowCollectResultRetry(statusMessage); } - await _bankIdEventTrigger.TriggerAsync(new BankIdCollectFailureEvent(collectResponse.OrderRef, collectResponse.GetCollectHintCode(), detectedUserDevice, flowOptions)); + await bankIdEventTrigger.TriggerAsync(new BankIdCollectFailureEvent(collectResponse.OrderRef, collectResponse.GetCollectHintCode(), detectedUserDevice, flowOptions)); return new BankIdFlowCollectResultFailure(statusMessage); } default: { - await _bankIdEventTrigger.TriggerAsync(new BankIdCollectFailureEvent(collectResponse.OrderRef, collectResponse.GetCollectHintCode(), detectedUserDevice, flowOptions)); + await bankIdEventTrigger.TriggerAsync(new BankIdCollectFailureEvent(collectResponse.OrderRef, collectResponse.GetCollectHintCode(), detectedUserDevice, flowOptions)); return new BankIdFlowCollectResultFailure(statusMessage); } } @@ -396,11 +347,11 @@ private async Task GetCollectResponse(string orderRef, BankIdFl { try { - return await _bankIdAppApiClient.CollectAsync(orderRef); + return await bankIdAppApiClient.CollectAsync(orderRef); } catch (BankIdApiException bankIdApiException) { - await _bankIdEventTrigger.TriggerAsync(new BankIdCollectErrorEvent(orderRef, bankIdApiException, detectedUserDevice, flowOptions)); + await bankIdEventTrigger.TriggerAsync(new BankIdCollectErrorEvent(orderRef, bankIdApiException, detectedUserDevice, flowOptions)); throw; } } @@ -410,36 +361,34 @@ private string GetStatusMessage(CollectResponse collectResponse, BankIdFlowOptio var accessedFromMobileDevice = detectedDevice.DeviceType == BankIdSupportedDeviceType.Mobile; var usingQrCode = !unprotectedFlowOptions.SameDevice; - var messageShortName = _bankIdUserMessage.GetMessageShortNameForCollectResponse( + var messageShortName = bankIdUserMessage.GetMessageShortNameForCollectResponse( collectResponse.GetCollectStatus(), collectResponse.GetCollectHintCode(), accessedFromMobileDevice, usingQrCode ); - var statusMessage = _bankIdUserMessageLocalizer.GetLocalizedString(messageShortName); - return statusMessage; + return bankIdUserMessageLocalizer.GetLocalizedString(messageShortName); } public string GetQrCodeAsBase64(BankIdQrStartState qrStartState) { - var elapsedTime = _bankIdFlowSystemClock.UtcNow - qrStartState.QrStartTime; + var elapsedTime = bankIdFlowSystemClock.UtcNow - qrStartState.QrStartTime; var elapsedTotalSeconds = (int)Math.Round(elapsedTime.TotalSeconds); var qrCodeContent = BankIdQrCodeContentGenerator.Generate(qrStartState.QrStartToken, qrStartState.QrStartSecret, elapsedTotalSeconds); - var qrCode = _bankIdQrCodeGenerator.GenerateQrCodeAsBase64(qrCodeContent); - return qrCode; + return bankIdQrCodeGenerator.GenerateQrCodeAsBase64(qrCodeContent); } public async Task Cancel(string orderRef, BankIdFlowOptions flowOptions) { - var detectedDevice = _bankIdSupportedDeviceDetector.Detect(); + var detectedDevice = bankIdSupportedDeviceDetector.Detect(); try { - await _bankIdAppApiClient.CancelAsync(orderRef); - await _bankIdEventTrigger.TriggerAsync(new BankIdCancelSuccessEvent(orderRef, detectedDevice, flowOptions)); + await bankIdAppApiClient.CancelAsync(orderRef); + await bankIdEventTrigger.TriggerAsync(new BankIdCancelSuccessEvent(orderRef, detectedDevice, flowOptions)); } catch (BankIdApiException exception) { @@ -447,7 +396,7 @@ public async Task Cancel(string orderRef, BankIdFlowOptions flowOptions) // are that the orderRef has already been cancelled or we have // a network issue. We still want to provide the GUI with the // validated cancellation URL though. - await _bankIdEventTrigger.TriggerAsync(new BankIdCancelErrorEvent(orderRef, exception, detectedDevice, flowOptions)); + await bankIdEventTrigger.TriggerAsync(new BankIdCancelErrorEvent(orderRef, exception, detectedDevice, flowOptions)); } } } diff --git a/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdDevelopmentLauncher.cs b/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdDevelopmentLauncher.cs index 28f46314..7292b3e9 100644 --- a/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdDevelopmentLauncher.cs +++ b/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdDevelopmentLauncher.cs @@ -3,11 +3,10 @@ namespace ActiveLogin.Authentication.BankId.Core.Launcher; internal class BankIdDevelopmentLauncher : IBankIdLauncher { private const string DevelopmentLaunchUrl = "#"; - private const string DevelopmentReturnUrl = ""; public Task GetLaunchInfoAsync(LaunchUrlRequest request) { // Always stay on same page, without reloading, in simulated mode - return Task.FromResult(new BankIdLaunchInfo(DevelopmentLaunchUrl, false, false, DevelopmentReturnUrl)); + return Task.FromResult(new BankIdLaunchInfo(DevelopmentLaunchUrl, false, false)); } } diff --git a/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLaunchInfo.cs b/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLaunchInfo.cs index ea8a343d..f226248b 100644 --- a/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLaunchInfo.cs +++ b/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLaunchInfo.cs @@ -1,90 +1,38 @@ -using ActiveLogin.Authentication.BankId.Api.Models; - namespace ActiveLogin.Authentication.BankId.Core.Launcher; -public class BankIdLaunchInfo +/// +/// Information about the launch of the BankID app. +/// +/// The URL used to launch the BankID app. +/// +/// True if the device/browser might require user interaction, such as a button click, to launch the BankID app. +/// +public class BankIdLaunchInfo( + string launchUrl, + bool deviceMightRequireUserInteractionToLaunchBankIdApp) { - /// - /// Creates a new instance. - /// - /// The URL used to launch the BankID app. - /// - /// True if the device/browser might require user interaction, such as a button click, to launch the BankID app. - /// - /// - /// True if the device/browser will reload the page when returning from the BankID app. - /// - /// - /// The return URL to send to BankID as part of the backend call when creating the order (auth, sign, or payment). - /// This URL will be called when the order completes. It is recommended to provide the return URL - /// via the backend call rather than including it in the autolaunch URL. - /// - public BankIdLaunchInfo( - string launchUrl, - bool deviceMightRequireUserInteractionToLaunchBankIdApp, - bool deviceWillReloadPageOnReturnFromBankIdApp, - string returnUrl) - { - LaunchUrl = launchUrl; - DeviceMightRequireUserInteractionToLaunchBankIdApp = deviceMightRequireUserInteractionToLaunchBankIdApp; - DeviceWillReloadPageOnReturnFromBankIdApp = deviceWillReloadPageOnReturnFromBankIdApp; - ReturnUrl = returnUrl; - } - /// - /// DEPRECATED: Use the constructor that requires returnUrl. - /// This overload will be removed in a future release. - /// - /// The URL used to launch the BankID app. - /// - /// True if the device/browser might require user interaction, such as a button click, to launch the BankID app. - /// - /// - /// True if the device/browser will reload the page when returning from the BankID app. - /// - [Obsolete("Use the constructor that requires 'returnUrl'. This overload will be removed in a future release.")] - public BankIdLaunchInfo( - string launchUrl, - bool deviceMightRequireUserInteractionToLaunchBankIdApp, - bool deviceWillReloadPageOnReturnFromBankIdApp) + public BankIdLaunchInfo(string launchUrl, bool deviceMightRequireUserInteractionToLaunchBankIdApp, bool deviceWillReloadPageOnReturnFromBankIdApp) + : this(launchUrl, deviceMightRequireUserInteractionToLaunchBankIdApp) { - LaunchUrl = launchUrl; - DeviceMightRequireUserInteractionToLaunchBankIdApp = deviceMightRequireUserInteractionToLaunchBankIdApp; - DeviceWillReloadPageOnReturnFromBankIdApp = deviceWillReloadPageOnReturnFromBankIdApp; - ReturnUrl = null; + // For backward compatibility } /// /// Returns the url used to launch the BankID app. /// - public string LaunchUrl { get; } + public string LaunchUrl { get; } = launchUrl; /// /// If the device/browser might require user interaction, such as button click, to launch a third party app. /// /// - public bool DeviceMightRequireUserInteractionToLaunchBankIdApp { get; } + public bool DeviceMightRequireUserInteractionToLaunchBankIdApp { get; } = deviceMightRequireUserInteractionToLaunchBankIdApp; /// /// If the device/browser will reload the page on return from the BankID app. /// /// + [Obsolete("This will be determined by the returnUrl sent to BankId")] public bool DeviceWillReloadPageOnReturnFromBankIdApp { get; } - - /// - /// Return URL used when the order is started on the same device - /// where the user’s BankID is installed (i.e using the ). - /// When the order completes, the BankID app will redirect to this URL. - /// - /// If both a return URL is specified here and one was provided as part of the , - /// using the redirect parameter. BankID will ignore the one in the redirect paramter and use this value instead. - /// - /// If the user has a version of the BankID app that does not support getting the returnUrl from the server, - /// the order will be cancelled and the user will be asked to update their app. - /// - /// The return URL you provide should include a nonce to the session. - /// When the user returns to your app or web page, your service should verify that - /// the device receiving the returnUrl is the same device that started the order. - /// - public string? ReturnUrl { get; } } diff --git a/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncher.cs b/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncher.cs index 6e1c6075..ff28a8c6 100644 --- a/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncher.cs +++ b/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncher.cs @@ -5,43 +5,28 @@ namespace ActiveLogin.Authentication.BankId.Core.Launcher; -internal class BankIdLauncher : IBankIdLauncher +internal class BankIdLauncher( + IBankIdSupportedDeviceDetector bankIdSupportedDeviceDetector, + ICustomBrowserResolver customBrowserResolver +) : IBankIdLauncher { private const string BankIdSchemePrefix = "bankid:///"; private const string BankIdAppLinkPrefix = "https://app.bankid.com/"; private const string BankIdAutoStartTokenQueryStringParamName = "autostarttoken"; private const string BankIdRpRefQueryStringParamName = "rpref"; - private const string BankIdRedirectQueryStringParamName = "redirect"; - private const string NullRedirectUrl = "null"; - - private const string IosChromeScheme = "googlechromes://"; - private const string IosFirefoxScheme = "firefox://"; - - private readonly IBankIdSupportedDeviceDetector _bankIdSupportedDeviceDetector; - private readonly List _customBrowsers; - - public BankIdLauncher(IBankIdSupportedDeviceDetector bankIdSupportedDeviceDetector, IEnumerable customBrowsers) - { - _bankIdSupportedDeviceDetector = bankIdSupportedDeviceDetector; - _customBrowsers = customBrowsers.ToList(); - } + private readonly IBankIdSupportedDeviceDetector _bankIdSupportedDeviceDetector = bankIdSupportedDeviceDetector; public async Task GetLaunchInfoAsync(LaunchUrlRequest request) { var detectedDevice = _bankIdSupportedDeviceDetector.Detect(); - - var customBrowserContext = new BankIdLauncherCustomBrowserContext(detectedDevice, request); - var customBrowser = await GetRelevantCustomAppCallbackAsync(customBrowserContext, _customBrowsers); - var customBrowserConfig = customBrowser != null ? (await customBrowser.GetCustomAppCallbackResult(customBrowserContext)) : null; + var customBrowserConfig = await customBrowserResolver.GetConfig(request); var deviceMightRequireUserInteractionToLaunch = GetDeviceMightRequireUserInteractionToLaunchBankIdApp(detectedDevice, customBrowserConfig); - var deviceWillReloadPageOnReturn = GetDeviceWillReloadPageOnReturnFromBankIdApp(detectedDevice, customBrowserConfig); - var launchUrl = GetLaunchUrl(detectedDevice, request, customBrowserConfig); - var returnUrl = GetRedirectUrl(detectedDevice, request, customBrowserConfig); + var launchUrl = GetLaunchUrl(detectedDevice, request); - return new BankIdLaunchInfo(launchUrl, deviceMightRequireUserInteractionToLaunch, deviceWillReloadPageOnReturn, returnUrl); + return new BankIdLaunchInfo(launchUrl, deviceMightRequireUserInteractionToLaunch); } private bool GetDeviceMightRequireUserInteractionToLaunchBankIdApp(BankIdSupportedDevice detectedDevice, BankIdLauncherCustomBrowserConfig? customBrowserConfig) @@ -58,39 +43,24 @@ private bool GetDeviceMightRequireUserInteractionToLaunchBankIdApp(BankIdSupport // // - Chrome, Edge, Samsung Internet Browser and Brave is confirmed to require User Interaction // - Firefox and Opera is confirmed to work without User Interaction - _ => detectedDevice.DeviceOs == BankIdSupportedDeviceOs.Android - && detectedDevice.DeviceBrowser != BankIdSupportedDeviceBrowser.Firefox - && detectedDevice.DeviceBrowser != BankIdSupportedDeviceBrowser.Opera - }; - } - - private bool GetDeviceWillReloadPageOnReturnFromBankIdApp(BankIdSupportedDevice detectedDevice, BankIdLauncherCustomBrowserConfig? customBrowserConfig) - { - var reloadBehaviour = customBrowserConfig?.BrowserReloadBehaviourOnReturnFromBankIdApp ?? BrowserReloadBehaviourOnReturnFromBankIdApp.Default; - - return reloadBehaviour switch - { - BrowserReloadBehaviourOnReturnFromBankIdApp.Always => true, - BrowserReloadBehaviourOnReturnFromBankIdApp.Never => false, - - // By default, Safari on iOS will refresh the page/tab when returned from the BankID app - _ => detectedDevice is - { - DeviceOs: BankIdSupportedDeviceOs.Ios, - DeviceBrowser: BankIdSupportedDeviceBrowser.Safari + // + // On iOS, browsers do not have this restriction + _ => detectedDevice is { + DeviceOs: BankIdSupportedDeviceOs.Android, + DeviceBrowser: not (BankIdSupportedDeviceBrowser.Firefox or BankIdSupportedDeviceBrowser.Opera) } }; } - private string GetLaunchUrl(BankIdSupportedDevice device, LaunchUrlRequest request, BankIdLauncherCustomBrowserConfig? customBrowserConfig) + private static string GetLaunchUrl(BankIdSupportedDevice device, LaunchUrlRequest request) { var prefix = GetPrefixPart(device); - var queryString = GetQueryStringPart(device, request, customBrowserConfig); + var queryString = GetQueryStringPart(request); return $"{prefix}{queryString}"; } - private string GetPrefixPart(BankIdSupportedDevice device) + private static string GetPrefixPart(BankIdSupportedDevice device) { return CanUseAppLink(device) ? BankIdAppLinkPrefix @@ -115,7 +85,7 @@ private static bool CanUseAppLink(BankIdSupportedDevice device) }; } - private string GetQueryStringPart(BankIdSupportedDevice device, LaunchUrlRequest request, BankIdLauncherCustomBrowserConfig? customBrowserConfig) + private static string GetQueryStringPart(LaunchUrlRequest request) { var queryStringParams = new Dictionary(); @@ -129,67 +99,9 @@ private string GetQueryStringPart(BankIdSupportedDevice device, LaunchUrlRequest queryStringParams.Add(BankIdRpRefQueryStringParamName, Base64Encode(request.RelyingPartyReference)); } - var redirectUrl = GetRedirectUrl(device, request, customBrowserConfig); - - // DEPRECATED: Setting the return URL here is no longer the recommended approach. - // The return URL should now be specified in the backend call to BankID when - // creating the order (auth, sign, or payment). - // This parameter is kept only for backward compatibility. If both values are set, - // BankID will use the one provided in the backend call. - queryStringParams.Add(BankIdRedirectQueryStringParamName, redirectUrl); - return QueryStringGenerator.ToQueryString(queryStringParams); } - private static string GetRedirectUrl(BankIdSupportedDevice device, LaunchUrlRequest request, BankIdLauncherCustomBrowserConfig? customBrowserConfig) - { - // Allow for easy override of callback url - if (customBrowserConfig != null && customBrowserConfig.ReturnUrl != null) - { - return customBrowserConfig.ReturnUrl; - } - - // Only use redirect url for iOS as recommended in BankID Guidelines 3.1.2 - return device.DeviceOs == BankIdSupportedDeviceOs.Ios - ? GetIOsBrowserSpecificRedirectUrl(device, request.RedirectUrl, customBrowserConfig) - : NullRedirectUrl; - } - - private static string GetIOsBrowserSpecificRedirectUrl(BankIdSupportedDevice device, string redirectUrl, BankIdLauncherCustomBrowserConfig? customBrowserConfig) - { - // If it is a third party browser, don't specify the return URL, just the browser scheme. - // This will launch the browser with the last page used (the Active Login status page). - // If a URL is specified these browsers will open that URL in a new tab and we will lose context. - - return device.DeviceBrowser switch - { - // Safari can only be launched by providing redirect url (https://...) - BankIdSupportedDeviceBrowser.Safari => redirectUrl, - - // Normally you would supply the URL, but we just want to launch the app again - BankIdSupportedDeviceBrowser.Chrome => IosChromeScheme, - BankIdSupportedDeviceBrowser.Firefox => IosFirefoxScheme, - - BankIdSupportedDeviceBrowser.Edge => NullRedirectUrl, - BankIdSupportedDeviceBrowser.Opera => NullRedirectUrl, - - _ => NullRedirectUrl - }; - } - - private static async Task GetRelevantCustomAppCallbackAsync(BankIdLauncherCustomBrowserContext customBrowserContext, List customAppCallbacks) - { - foreach (var callback in customAppCallbacks) - { - if (await callback.IsApplicable(customBrowserContext)) - { - return callback; - } - } - - return null; - } - private static string Base64Encode(string value) { var encodedBytes = Encoding.Unicode.GetBytes(value); diff --git a/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncherCustomBrowserByContext.cs b/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncherCustomBrowserByContext.cs index 9093c477..9fdd7c88 100644 --- a/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncherCustomBrowserByContext.cs +++ b/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncherCustomBrowserByContext.cs @@ -1,25 +1,17 @@ namespace ActiveLogin.Authentication.BankId.Core.Launcher; -public class BankIdLauncherCustomBrowserByContext : IBankIdLauncherCustomBrowser +public class BankIdLauncherCustomBrowserByContext( + Func isApplicable, + Func getResult +) : IBankIdLauncherCustomBrowser { - private readonly Func _isApplicable; - private readonly Func _getResult; - - public BankIdLauncherCustomBrowserByContext(Func isApplicable, Func getResult) - { - _isApplicable = isApplicable; - _getResult = getResult; - } - public Task IsApplicable(BankIdLauncherCustomBrowserContext context) { - var isApplicable = _isApplicable(context); - return Task.FromResult(isApplicable); + return Task.FromResult(isApplicable(context)); } public Task GetCustomAppCallbackResult(BankIdLauncherCustomBrowserContext context) { - var result = _getResult(context); - return Task.FromResult(result); + return Task.FromResult(getResult(context)); } } diff --git a/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncherCustomBrowserConfig.cs b/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncherCustomBrowserConfig.cs index 10e7b5b2..95ca74cc 100644 --- a/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncherCustomBrowserConfig.cs +++ b/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncherCustomBrowserConfig.cs @@ -1,8 +1,31 @@ namespace ActiveLogin.Authentication.BankId.Core.Launcher; +public record BrowserScheme(string scheme) +{ + // we need to trim any trailing :// to make it easier to use + private readonly string _value = scheme.TrimEnd(':', '/'); + + public override string ToString() => _value; + public static implicit operator string(BrowserScheme browserScheme) => browserScheme._value; +} + public class BankIdLauncherCustomBrowserConfig { - public BankIdLauncherCustomBrowserConfig(string? returnUrl, BrowserReloadBehaviourOnReturnFromBankIdApp browserReloadBehaviourOnReturnFromBankIdApp = BrowserReloadBehaviourOnReturnFromBankIdApp.Default, BrowserMightRequireUserInteractionToLaunch browserMightRequireUserInteractionToLaunch = BrowserMightRequireUserInteractionToLaunch.Default) + public BankIdLauncherCustomBrowserConfig( + BrowserScheme browserScheme, + BrowserMightRequireUserInteractionToLaunch browserMightRequireUserInteractionToLaunch + ) + { + BrowserScheme = browserScheme; + BrowserMightRequireUserInteractionToLaunch = browserMightRequireUserInteractionToLaunch; + } + + [Obsolete("Specifying ReturnUrl is deprecated, use BrowserScheme instead.")] + public BankIdLauncherCustomBrowserConfig( + string? returnUrl, + BrowserReloadBehaviourOnReturnFromBankIdApp browserReloadBehaviourOnReturnFromBankIdApp = BrowserReloadBehaviourOnReturnFromBankIdApp.Default, + BrowserMightRequireUserInteractionToLaunch browserMightRequireUserInteractionToLaunch = BrowserMightRequireUserInteractionToLaunch.Default + ) { ReturnUrl = returnUrl; BrowserReloadBehaviourOnReturnFromBankIdApp = browserReloadBehaviourOnReturnFromBankIdApp; @@ -15,11 +38,14 @@ public BankIdLauncherCustomBrowserConfig(string? returnUrl, BrowserReloadBehavio /// Set to empty string to not launch any URL, and instead the BanKID app will ask the user to open the last app. /// This will only be applied to iOS as Android automatically launches the previous app. /// + [Obsolete("Deprecated in favor of only specifying BrowserScheme")] public string? ReturnUrl { get; set; } + public BrowserScheme? BrowserScheme { get; set; } /// /// The reload behaviour of the browser when returning from the BankID app. /// + [Obsolete("This setting is deprecated and will be removed in future versions.")] public BrowserReloadBehaviourOnReturnFromBankIdApp BrowserReloadBehaviourOnReturnFromBankIdApp { get; set; } /// diff --git a/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdRedirectUrl.cs b/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdRedirectUrl.cs new file mode 100644 index 00000000..de5ce45c --- /dev/null +++ b/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdRedirectUrl.cs @@ -0,0 +1,156 @@ +using System.Diagnostics.CodeAnalysis; + +using ActiveLogin.Authentication.BankId.Core.SupportedDevice; + +namespace ActiveLogin.Authentication.BankId.Core.Launcher; + + +/* +Android with Edge: bankid:// +*/ + +/// +/// The redirect URL:
+/// * Must be UTF-8 and URL encoded.
+/// * Should take the user back to the same webpage.
+/// * May include parameters to be passed to the browser.
+/// * Can be set to null. +///
+public record BankIdRedirectUrl +{ + private const string IosChromeScheme = "chromebrowser"; + private const string IosFirefoxScheme = "firefox"; + + public required string Url { get; init; } + + private BankIdRedirectUrl(){} + + public static Result TryCreate( + string redirectUrl, + BankIdLauncherCustomBrowserConfig? config, + BankIdSupportedDevice device + ) + { + if (string.IsNullOrWhiteSpace(redirectUrl)) + { + return "Invalid URL"; + } + + // only https allowed + if (!redirectUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + return "Only https scheme is allowed in redirect URL"; + } + + // get custom browser scheme if any, otherwise get scheme based on device + var browserScheme = config?.BrowserScheme is not null + ? config.BrowserScheme + : GetScheme(device); + + redirectUrl = !string.IsNullOrWhiteSpace(browserScheme) + ? redirectUrl.Replace("https", browserScheme, StringComparison.OrdinalIgnoreCase) + : redirectUrl; + + var url = Uri.EscapeDataString(redirectUrl); + + if (512 < url.Length) + { + return "URL must be at most 512 characters long"; + } + + return new BankIdRedirectUrl + { + Url = url + }; + } + + private static string? GetScheme(BankIdSupportedDevice device) + { + // This will launch the browser with the last page used (the Active Login status page). + // If a URL is specified these browsers will open that URL in a new tab and we will lose context. + return device.DeviceOs switch + { + BankIdSupportedDeviceOs.Ios => device.DeviceBrowser switch + { + BankIdSupportedDeviceBrowser.Chrome => IosChromeScheme, // Normally you would supply the URL, but we just want to launch the app again + BankIdSupportedDeviceBrowser.Firefox => IosFirefoxScheme, // Normally you would supply the URL, but we just want to launch the app again + _ => null + }, + _ => null + }; + } + + public override string ToString() => Url; + + [return: NotNullIfNotNull(nameof(bankIdRedirectUrl))] + public static implicit operator string?(BankIdRedirectUrl? bankIdRedirectUrl) => bankIdRedirectUrl?.Url; +}; + +/* +# Return URL +When the BankID client is used on the same device as the user visits your service on, +the user should be sent back to your service once they've completed their action in the BankID client. +From version 6 of our RP-API, it's possible and strongly recommended to send the return URL via server, +meaning that you can protect the user and ensure that any sessions started from your service will send them back to your service. +This can mitigate some session fixation cases. + +Note that usage of return URL as part of autostart URL is deprecated. Instead, the return URL +should be included in the RP-API call when starting an order. + +## How it works + +### For Web +1. The user visits your website at https://www.example.com +2. The user chooses to log in. https://www.example.com/login +3. Your website makes a call to our RP-API which includes, among other things, the return URL: https://www.example.com/login#nonce=[session nonce] +4. The RP-API returns the autostart token. +5. Your website starts the BankID client using the autostart URL. Example: https://app.bankid.com/?autostarttoken=[autostarttoken from step 4] +6. The user completes the identification in the BankID app. +7. The BankID app invokes the return URL, previously sent to the RP-API in step 3. +8. Your website verifies that the nonce matches the session nonce. + +### For App +1. The user starts your app. +2. The user chooses to log in. +3. Your app makes a server side call to our RP-API, which includes, among other things, the app specific return URL: https://app.example.com/login#nonce=[session nonce] or myapp://login#nonce=[session nonce]. +4. The RP-API returns the autostart token. +5. Your app starts the BankID client using the autostart URL. Example: https://app.bankid.com/?autostarttoken=[autostarttoken from step 4] +6. The user completes the identification in the BankID app. +7. The BankID app invokes the return URL, previously sent to the RP-API in step 3. +8. Your app verifies that the nonce matches the session nonce. + +Please note: If the user has an older desktop version of the BankID client that doesn't support getting the +return URL from the server, the order will be cancelled and the user will be asked to update their app. + +## Pitfalls in web flows +In cases where the user visits your service in a web browser you can face some challenges when it comes to returning +them to the right broswser or app. This is due to device settings and/or differences in browser behaviour and is +out of our control. However, there are some things you can do to enhance the probability that the user is +returned to the right browser and/or tab. + +## Returning the user to correct browser +The return URL will be opened in the default browser, even if the user started the session in a different browser. + +If the used brower has a documented URL scheme this should be used in the return URL instead of 'https'. + +Example: https://www.example.com/ for Google Chrome would be: chromebrowser://www.example.com/. + +Please note: For browser without a documented URL scheme, the user will be sent to the default browser. + +## Returning the user to correct tab +For a good user experience, returning the user to the same tab is preferred. +However, it can be a challenge to do this from an app, such as BankID security app. + +The highest probability to return the user to the tab where they started BankID from, +is to use the exact URL of the target tab as the return URL. +However, to bind the new request to the old browser session, +the return URL should include a nonce that server side will match to the old browser session. +The best way to accomplish this is to place the nonce in the URL fragment. +Please note that the nonce should not contain sensitive session information such as the session cookie. + +Example: https://www.example.com/login#nonce=[session nonce] + +## Returning the user to correct browser and tab +When combining the methods above, some browsers may deliver unexpected results. This is because different browser behave differently when given the same input. It's important that you test different scenarios for your service. + +*/ diff --git a/src/ActiveLogin.Authentication.BankId.Core/Launcher/IBankIdRedirectUrlResolver.cs b/src/ActiveLogin.Authentication.BankId.Core/Launcher/IBankIdRedirectUrlResolver.cs new file mode 100644 index 00000000..f1bc8251 --- /dev/null +++ b/src/ActiveLogin.Authentication.BankId.Core/Launcher/IBankIdRedirectUrlResolver.cs @@ -0,0 +1,13 @@ +namespace ActiveLogin.Authentication.BankId.Core.Launcher; + +public enum BankIdTransactionType +{ + Auth, + Sign, + Payment +} + +public interface IBankIdRedirectUrlResolver +{ + Task GetRedirectUrl(BankIdTransactionType type, string callbackPath); +} diff --git a/src/ActiveLogin.Authentication.BankId.Core/Launcher/ICustomBrowserResolver.cs b/src/ActiveLogin.Authentication.BankId.Core/Launcher/ICustomBrowserResolver.cs new file mode 100644 index 00000000..a1d2c142 --- /dev/null +++ b/src/ActiveLogin.Authentication.BankId.Core/Launcher/ICustomBrowserResolver.cs @@ -0,0 +1,6 @@ +namespace ActiveLogin.Authentication.BankId.Core.Launcher; + +public interface ICustomBrowserResolver +{ + Task GetConfig(LaunchUrlRequest launchUrlRequest); +} diff --git a/src/ActiveLogin.Authentication.BankId.Core/Option.cs b/src/ActiveLogin.Authentication.BankId.Core/Option.cs new file mode 100644 index 00000000..28b8c473 --- /dev/null +++ b/src/ActiveLogin.Authentication.BankId.Core/Option.cs @@ -0,0 +1,36 @@ +namespace System; + +public abstract record Option +{ + public sealed record Some(T Value) : Option; + public sealed record None() : Option; + + public bool IsSome => this is Some; + public bool IsNone => this is None; + + public T UnwrapOr(T defaultValue) => this is Some(var value) ? value : defaultValue; + + public void Match(Action onSome, Action onNone) + { + if (this is Some(var value)) + { + onSome(value); + } + else if (this is None) + { + onNone(); + } + } + + public Option Map(Func f) + { + return this switch + { + Some(var value) => new Some(f(value)), + None => new None(), + _ => throw new InvalidOperationException("Unrecognized option type") + }; + } + + public static implicit operator Option(T value) => new Some(value); +} diff --git a/src/ActiveLogin.Authentication.BankId.Core/Result.cs b/src/ActiveLogin.Authentication.BankId.Core/Result.cs new file mode 100644 index 00000000..0b20f8cf --- /dev/null +++ b/src/ActiveLogin.Authentication.BankId.Core/Result.cs @@ -0,0 +1,61 @@ +namespace System; + +public abstract record Result +{ + public static Result From(T value, string? errorMessage) + { + return errorMessage is null + ? new Success(value) + : new Failure(errorMessage); + } + + public sealed record Success(T Value) : Result; + public sealed record Failure(string ErrorMessage) : Result; + + public bool IsSuccess => this is Success; + public bool IsFailure => this is Failure; + + /// + /// Maps the result value if success, otherwise propagates the failure. + /// + /// + /// + /// + /// + public Result Map(Func f) + { + return this switch + { + Success(var value) => new Result.Success(f(value)), + Failure(var errorMessage) => new Result.Failure(errorMessage), + _ => throw new InvalidOperationException("Unrecognized result type") + }; + } + + /// + /// Pattern match on the result. + /// + /// + /// + public void Match(Action onSuccess, Action onFailure) + { + if (this is Success(var value)) + { + onSuccess(value); + } + else if (this is Failure(var errorMessage)) + { + onFailure(errorMessage); + } + } + + /// + /// Unwraps the result value if success, otherwise returns the provided default value. + /// + /// + /// + public T UnwrapOr(T defaultValue) => this is Success(var value) ? value : defaultValue; + + public static implicit operator Result(T value) => new Success(value); + public static implicit operator Result(string errorMessage) => new Failure(errorMessage); +} diff --git a/src/ActiveLogin.Authentication.BankId.Core/ServiceCollectionBankIdExtensions.cs b/src/ActiveLogin.Authentication.BankId.Core/ServiceCollectionBankIdExtensions.cs index 80872188..260ef99e 100644 --- a/src/ActiveLogin.Authentication.BankId.Core/ServiceCollectionBankIdExtensions.cs +++ b/src/ActiveLogin.Authentication.BankId.Core/ServiceCollectionBankIdExtensions.cs @@ -50,7 +50,7 @@ public static IServiceCollection AddBankId(this IServiceCollection services, Act UseUserAgentFromContext(bankIdBuilder); bankId(bankIdBuilder); - + return services; } @@ -75,7 +75,7 @@ private static void AddBankIdDefaultServices(IBankIdBuilder builder) builder.AddEventListener(); builder.AddResultStore(); - + } private static void UseUserAgentFromContext(this IBankIdBuilder builder) @@ -92,7 +92,7 @@ private static void ConfigureHttpClientWithUserAgent(IServiceProvider sp, HttpCl httpClient.DefaultRequestHeaders.UserAgent.Clear(); httpClient.DefaultRequestHeaders.UserAgent.Add(productInfoHeaderValue); } - + private static (string name, string version) GetActiveLoginInfo() { var productName = BankIdConstants.ProductName; diff --git a/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/BankId_UiAuth_Tests.cs b/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/BankId_UiAuth_Tests.cs index bf3f19e6..c59f7c6d 100644 --- a/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/BankId_UiAuth_Tests.cs +++ b/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/BankId_UiAuth_Tests.cs @@ -334,60 +334,6 @@ public async Task Init_Requires_State_And_UiOptions_Cookie_To_Be_Present() Assert.Equal("/", transaction.Headers.Location.ToString()); } - [Fact] - public async Task AutoLaunch_Sets_Correct_RedirectUri() - { - // Arrange mocks - var autoLaunchOptions = - new BankIdUiOptions(new List(), true, false, false, false, string.Empty, DefaultStateCookieName, Api.Models.CardReader.class1); - var mockProtector = new Mock(); - mockProtector - .Setup(protector => protector.Unprotect(It.IsAny())) - .Returns(autoLaunchOptions); - mockProtector - .Setup(protector => protector.Protect(It.IsAny())) - .Returns("Ignored"); - - using var server = CreateServer( - o => - { - o.UseSimulatedEnvironment(); - o.Services.AddTransient(); - }, - o => - { - o.AddSameDevice(); - }, - DefaultAppConfiguration(async context => - { - await context.ChallengeAsync(BankIdAuthDefaults.SameDeviceAuthenticationScheme); - }), - services => - { - services.AddTransient(s => mockProtector.Object); - services.AddTransient(s => _bankIdUiStateProtector.Object); - }); - - // Arrange acting request - var testReturnUrl = "/TestReturnUrl"; - var initializeRequestBody = new {returnUrl = testReturnUrl}; - - // Act - var initializeTransaction = await GetInitializeResponse(server, initializeRequestBody); - - // Assert - Assert.Equal(HttpStatusCode.OK, initializeTransaction.StatusCode); - - var responseContent = await initializeTransaction.Content.ReadAsStringAsync(); - var responseObject = JsonConvert.DeserializeAnonymousType(responseContent, - new {RedirectUri = "", OrderRef = "", IsAutoLaunch = false}); - Assert.True(responseObject.IsAutoLaunch); - - var encodedReturnParam = UrlEncoder.Default.Encode(testReturnUrl); - var expectedUrl = $"http://localhost/ActiveLogin/BankId/Auth?returnUrl={encodedReturnParam}"; - Assert.Equal(expectedUrl, responseObject.RedirectUri); - } - [Fact] public async Task Api_Always_Returns_CamelCase_Json_For_Http200Ok() { @@ -439,7 +385,7 @@ public async Task Api_Always_Returns_CamelCase_Json_For_Http200Ok() Assert.Equal(HttpStatusCode.OK, initializeTransaction.StatusCode); var responseContent = await initializeTransaction.Content.ReadAsStringAsync(); - Assert.Contains("redirectUri", responseContent); + Assert.Contains("launchUrl", responseContent); Assert.Contains("orderRef", responseContent); Assert.Contains("isAutoLaunch", responseContent); } @@ -568,6 +514,14 @@ private TestServer CreateServer( { var webHostBuilder = new WebHostBuilder() .UseSolutionRelativeContentRoot(Path.Combine("test", "ActiveLogin.Authentication.BankId.AspNetCore.Test")) + .ConfigureKestrel(options => + { + // https + options.ListenLocalhost(44300, listenOptions => + { + listenOptions.UseHttps(); + }); + }) .Configure(app => { configureApplication.Invoke(app); diff --git a/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/BankId_UiPayment_Tests.cs b/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/BankId_UiPayment_Tests.cs index bea272ec..3eb67fd0 100644 --- a/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/BankId_UiPayment_Tests.cs +++ b/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/BankId_UiPayment_Tests.cs @@ -338,55 +338,6 @@ public async Task PaymentInit_Requires_State_Cookie_To_Be_Present() Assert.Equal("/", transaction.Headers.Location.ToString()); } - [Fact] - public async Task AutoLaunch_Sets_Correct_RedirectUri() - { - // Arrange mocks - var autoLaunchOptions = new BankIdUiOptions(new List(), true, false, false, false, string.Empty, DefaultStateCookieName, CardReader.class1); - - _bankIdUiOptionsCookieManager - .Setup(protector => protector.Retrieve()) - .Returns(autoLaunchOptions); - - using var server = CreateServer( - o => - { - o.UseSimulatedEnvironment(); - o.Services.AddTransient(); - }, - o => - { - o.AddSameDevice(); - }, - DefaultAppConfiguration(async context => - { - await InitiatePayment(context); - }), - services => - { - services.AddTransient(s => _bankIdUiOptionsCookieManager.Object); - services.AddTransient(s => _bankIdUiStateProtector.Object); - }); - - // Arrange acting request - var testReturnUrl = "/TestReturnUrl"; - var initializeRequestBody = new { returnUrl = testReturnUrl}; - - // Act - var initializeTransaction = await GetInitializeResponse(server, initializeRequestBody); - - // Assert - Assert.Equal(HttpStatusCode.OK, initializeTransaction.StatusCode); - - var responseContent = await initializeTransaction.Content.ReadAsStringAsync(); - var responseObject = JsonConvert.DeserializeAnonymousType(responseContent, new { RedirectUri = "", OrderRef = "", IsAutoLaunch = false }); - Assert.True(responseObject.IsAutoLaunch); - - var encodedReturnParam = UrlEncoder.Default.Encode(testReturnUrl); - var expectedUrl = $"http://localhost/ActiveLogin/BankId/Payment?returnUrl={encodedReturnParam}"; - Assert.Equal(expectedUrl, responseObject.RedirectUri); - } - [Fact] public async Task Api_Always_Returns_CamelCase_Json_For_Http200Ok() { @@ -438,7 +389,7 @@ public async Task Api_Always_Returns_CamelCase_Json_For_Http200Ok() Assert.Equal(HttpStatusCode.OK, initializeTransaction.StatusCode); var responseContent = await initializeTransaction.Content.ReadAsStringAsync(); - Assert.Contains("redirectUri", responseContent); + Assert.Contains("launchUrl", responseContent); Assert.Contains("orderRef", responseContent); Assert.Contains("isAutoLaunch", responseContent); } @@ -565,7 +516,7 @@ private TestServer CreateServer( return new TestServer(webHostBuilder); } - + private static Action DefaultAppConfiguration(Func testpath) { return app => diff --git a/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/BankId_UiSign_Tests.cs b/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/BankId_UiSign_Tests.cs index 7dc3ed61..7c6eb619 100644 --- a/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/BankId_UiSign_Tests.cs +++ b/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/BankId_UiSign_Tests.cs @@ -30,8 +30,6 @@ using Moq; -using Newtonsoft.Json; - using Xunit; namespace ActiveLogin.Authentication.BankId.AspNetCore.Test; @@ -335,55 +333,6 @@ public async Task SignInit_Requires_State_Cookie_To_Be_Present() Assert.Equal("/", transaction.Headers.Location.ToString()); } - [Fact] - public async Task AutoLaunch_Sets_Correct_RedirectUri() - { - // Arrange mocks - var autoLaunchOptions = new BankIdUiOptions(new List(), true, false, false, false, string.Empty, DefaultStateCookieName, Api.Models.CardReader.class1); - - _bankIdUiOptionsCookieManager - .Setup(protector => protector.Retrieve()) - .Returns(autoLaunchOptions); - - using var server = CreateServer( - o => - { - o.UseSimulatedEnvironment(); - o.Services.AddTransient(); - }, - o => - { - o.AddSameDevice(); - }, - DefaultAppConfiguration(async context => - { - await InitiateSign(context); - }), - services => - { - services.AddTransient(s => _bankIdUiOptionsCookieManager.Object); - services.AddTransient(s => _bankIdUiStateProtector.Object); - }); - - // Arrange acting request - var testReturnUrl = "/TestReturnUrl"; - var initializeRequestBody = new { returnUrl = testReturnUrl}; - - // Act - var initializeTransaction = await GetInitializeResponse(server, initializeRequestBody); - - // Assert - Assert.Equal(HttpStatusCode.OK, initializeTransaction.StatusCode); - - var responseContent = await initializeTransaction.Content.ReadAsStringAsync(); - var responseObject = JsonConvert.DeserializeAnonymousType(responseContent, new { RedirectUri = "", OrderRef = "", IsAutoLaunch = false }); - Assert.True(responseObject.IsAutoLaunch); - - var encodedReturnParam = UrlEncoder.Default.Encode(testReturnUrl); - var expectedUrl = $"http://localhost/ActiveLogin/BankId/Sign?returnUrl={encodedReturnParam}"; - Assert.Equal(expectedUrl, responseObject.RedirectUri); - } - [Fact] public async Task Api_Always_Returns_CamelCase_Json_For_Http200Ok() { @@ -435,7 +384,7 @@ public async Task Api_Always_Returns_CamelCase_Json_For_Http200Ok() Assert.Equal(HttpStatusCode.OK, initializeTransaction.StatusCode); var responseContent = await initializeTransaction.Content.ReadAsStringAsync(); - Assert.Contains("redirectUri", responseContent); + Assert.Contains("launchUrl", responseContent); Assert.Contains("orderRef", responseContent); Assert.Contains("isAutoLaunch", responseContent); } diff --git a/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/Helpers/TestBankIdLauncher.cs b/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/Helpers/TestBankIdLauncher.cs index b35fda0c..f490b7c9 100644 --- a/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/Helpers/TestBankIdLauncher.cs +++ b/test/ActiveLogin.Authentication.BankId.AspNetCore.Test/Helpers/TestBankIdLauncher.cs @@ -9,6 +9,6 @@ internal class TestBankIdLauncher : IBankIdLauncher public Task GetLaunchInfoAsync(LaunchUrlRequest request) { // Always redirect back without user interaction in simulated mode - return Task.FromResult(new BankIdLaunchInfo(request.RedirectUrl, false, false, request.RedirectUrl)); + return Task.FromResult(new BankIdLaunchInfo(request.RedirectUrl, false, false)); } } diff --git a/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdLauncher_Tests.cs b/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdLauncher_Tests.cs index 1c02718f..3bb25638 100644 --- a/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdLauncher_Tests.cs +++ b/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdLauncher_Tests.cs @@ -7,57 +7,52 @@ namespace ActiveLogin.Authentication.BankId.Core.Test; + +public class FakeBankIdSupportedDeviceDetector(BankIdSupportedDevice device) : IBankIdSupportedDeviceDetector +{ + public BankIdSupportedDevice Detect() => device; +} + +public class FakeCustomBrowserResolver(BankIdLauncherCustomBrowserConfig config = null) : ICustomBrowserResolver +{ + public Task GetConfig(LaunchUrlRequest launchUrlRequest) + { + return Task.FromResult(config); + } +} + +/// +/// Tests we want to do: +/// * Ensure that BankIdLauncher generates a valid LaunchInfo +/// * Test that BankIdLauncher uses detected device to determine if user interaction is needed to launch BankID app +/// * Test that BankIdLauncher uses the CustomBrowserResolver to determine if the browser will reload on return from BankID app +/// + public class BankIdLauncher_Tests { [Fact] - public async Task BankIdLauncher_Should_DefaultReloadBehavior() + public async Task BankIdLauncher_Should_GenerateValidLaunchInfo() { var launcher = new BankIdLauncher( - new TestBankIdSupportedDeviceDetector(), - System.Array.Empty()); + new FakeBankIdSupportedDeviceDetector(BankIdTestDevices.Mobile.Ios.Chrome), + new FakeCustomBrowserResolver() + ); - var info = await launcher.GetLaunchInfoAsync(new LaunchUrlRequest("", "")); + var info = await launcher.GetLaunchInfoAsync(new LaunchUrlRequest("https://localhost:5001/ActiveLogin/Auth/Init", "")); - Assert.False(info.DeviceWillReloadPageOnReturnFromBankIdApp); + Assert.NotNull(info); } [Fact] - public async Task BankIdLauncher_Should_UseReloadBehaviourWhenImplemented() + public async Task BankIdLauncher_Should_UseDetectedDeviceToDetermineUserInteraction() { var launcher = new BankIdLauncher( - new TestBankIdSupportedDeviceDetector(), - new [] { new TestBankIdLauncherCustomBrowser() }); // Override behaviour on return from BankID app - - var info = await launcher.GetLaunchInfoAsync(new LaunchUrlRequest(string.Empty, string.Empty)); - - Assert.True(info.DeviceWillReloadPageOnReturnFromBankIdApp); - } + new FakeBankIdSupportedDeviceDetector(BankIdTestDevices.Mobile.Ios.Chrome), + new FakeCustomBrowserResolver() + ); - private class TestBankIdLauncherCustomBrowser : IBankIdLauncherCustomBrowser - { - public Task IsApplicable(BankIdLauncherCustomBrowserContext context) - { - return Task.FromResult(true); - } - - public Task GetCustomAppCallbackResult(BankIdLauncherCustomBrowserContext context) - { - return Task.FromResult( - new BankIdLauncherCustomBrowserConfig("/return", BrowserReloadBehaviourOnReturnFromBankIdApp.Always) - ); - } - } + var info = await launcher.GetLaunchInfoAsync(new LaunchUrlRequest("https://localhost:5001/ActiveLogin/Auth/Init", "")); - private class TestBankIdSupportedDeviceDetector : IBankIdSupportedDeviceDetector - { - public BankIdSupportedDevice Detect() - { - // A device that will not reload the page on return from BankID app (Desktop Windows) - return new BankIdSupportedDevice( - BankIdSupportedDeviceType.Desktop, - BankIdSupportedDeviceOs.Windows, - BankIdSupportedDeviceBrowser.Chrome, - BankIdSupportedDeviceOsVersion.Empty); - } + Assert.False(info.DeviceMightRequireUserInteractionToLaunchBankIdApp); } } diff --git a/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdRedirectUriTests.cs b/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdRedirectUriTests.cs new file mode 100644 index 00000000..d57baca6 --- /dev/null +++ b/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdRedirectUriTests.cs @@ -0,0 +1,572 @@ +using System; + +using ActiveLogin.Authentication.BankId.Core.Launcher; + +using Xunit; + +namespace ActiveLogin.Authentication.BankId.Core.Test; + +/// +/// Comprehensive tests for BankIdRedirectUrl.TryCreate covering: +/// - URL validation (null, empty, whitespace) +/// - URL encoding requirements +/// - Maximum length validation (512 characters after encoding) +/// - Browser-specific scheme replacement (iOS Chrome and Firefox) +/// - Custom browser configuration override +/// - URL fragment preservation (for nonce binding) +/// - Different browser/OS combinations +/// +/// Based on BankID Return URL specification: +/// https://developers.bankid.com/getting-started/return-url +/// +public class BankIdRedirectUriTests +{ + #region Validation Tests + + [Fact] + public void TryCreate_ShouldFail_WhenUrlIsNull() + { + // Act + var result = BankIdRedirectUrl.TryCreate( + null!, + config: null, + device: BankIdTestDevices.AnyDevice); // Device doesn't affect null validation + + // Assert + result.Match( + _ => Assert.Fail("Expected failure but got success"), + error => Assert.Equal("Invalid URL", error) + ); + } + + [Fact] + public void TryCreate_ShouldFail_WhenUrlIsEmpty() + { + // Act + var result = BankIdRedirectUrl.TryCreate( + string.Empty, + config: null, + device: BankIdTestDevices.AnyDevice); // Device doesn't affect empty string validation + + // Assert + result.Match( + _ => Assert.Fail("Expected failure but got success"), + error => Assert.Equal("Invalid URL", error) + ); + } + + [Fact] + public void TryCreate_ShouldFail_WhenUrlIsWhitespace() + { + // Act + var result = BankIdRedirectUrl.TryCreate( + " ", + config: null, + device: BankIdTestDevices.AnyDevice); // Device doesn't affect whitespace validation + + // Assert + Assert.True(result.IsFailure); + result.Match( + _ => Assert.Fail("Expected failure but got success"), + error => Assert.Equal("Invalid URL", error) + ); + } + + [Fact] + public void TryCreate_ShouldFail_WhenEncodedUrlExceeds512Characters() + { + // Arrange - Create URL that when encoded exceeds 512 chars + var longUrl = "https://example.com/" + new string('a', 500); + + // Act + var result = BankIdRedirectUrl.TryCreate( + longUrl, + config: null, + device: BankIdTestDevices.AnyDevice); // Device doesn't affect length validation + + // Assert + Assert.True(result.IsFailure); + result.Match( + _ => Assert.Fail("Expected failure but got success"), + error => Assert.Equal("URL must be at most 512 characters long", error) + ); + } + + [Fact] + public void TryCreate_ShouldSucceed_WhenEncodedUrlIsExactly512Characters() + { + // Arrange - Calculate URL that encodes to exactly 512 characters + // "https://example.com/" encodes to "https%3A%2F%2Fexample.com%2F" (28 chars) + // Need 512 - 28 = 484 more encoded characters + // 'a' encodes to 'a' (1 char), so we need 484 'a's + var url = "https://example.com/" + new string('a', 484); + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device: BankIdTestDevices.AnyDevice); // Device doesn't affect length validation + + // Assert + Assert.True(result.IsSuccess, "URL with exactly 512 encoded characters should succeed"); + result.Match( + redirectUrl => Assert.Equal(512, redirectUrl.Url.Length), + _ => Assert.Fail("Expected success but got failure") + ); + } + + #endregion + + #region URL Encoding Tests + + [Fact] + public void TryCreate_ShouldEncodeUrl_UsingUriEscapeDataString() + { + // Arrange + var url = "https://example.com/path?param=value&other=test"; + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device: BankIdTestDevices.AnyDevice); // Device doesn't affect URL encoding + + // Assert + result.Match( + redirectUrl => { + Assert.Equal(Uri.EscapeDataString(url), redirectUrl.Url); + }, + _ => Assert.Fail("Expected success but got failure") + ); + } + + [Fact] + public void TryCreate_ShouldPreserveUrlFragment_ForNonceBinding() + { + // Arrange - Fragment with nonce as per BankID spec + var url = "https://example.com/login#nonce=session123"; + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device: BankIdTestDevices.AnyDevice); // Device doesn't affect fragment preservation + + // Assert + result.Match( + redirectUrl => { + var decodedUrl = Uri.UnescapeDataString(redirectUrl.Url); + Assert.Contains("#nonce=session123", decodedUrl); + }, + _ => Assert.Fail("Expected success but got failure") + ); + } + + [Fact] + public void TryCreate_ShouldEncodeSpecialCharacters() + { + // Arrange + var url = "https://example.com/path?special=åäö&chars=spaces here"; + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device: BankIdTestDevices.AnyDevice); // Device doesn't affect character encoding + + // Assert + result.Match( + redirectUrl => { + Assert.DoesNotContain("å", redirectUrl.Url); + Assert.DoesNotContain("ä", redirectUrl.Url); + Assert.DoesNotContain("ö", redirectUrl.Url); + Assert.DoesNotContain(" ", redirectUrl.Url); + }, + _ => Assert.Fail("Expected success but got failure") + ); + } + + #endregion + + #region iOS Browser Scheme Replacement Tests + + [Fact] + public void TryCreate_ShouldReplaceHttpsWithChromeScheme_ForIosChrome() + { + // Arrange + var url = "https://example.com/return?"; + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device: BankIdTestDevices.Mobile.Ios.Chrome); // Testing iOS Chrome-specific behavior + + // Assert + result.Match( + redirectUrl => { + var decodedUrl = Uri.UnescapeDataString(redirectUrl.Url); + Assert.StartsWith("chromebrowser://", decodedUrl); + }, + _ => Assert.Fail("Expected success but got failure") + ); + } + + [Fact] + public void TryCreate_ShouldReplaceHttpsWithFirefoxScheme_ForIosFirefox() + { + // Arrange + var url = "https://example.com/return"; + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device: BankIdTestDevices.Mobile.Ios.Firefox); // Testing iOS Firefox-specific behavior + + // Assert + result.Match( + redirectUrl => { + var decodedUrl = Uri.UnescapeDataString(redirectUrl.Url); + Assert.StartsWith("firefox://", decodedUrl); + }, + _ => Assert.Fail("Expected success but got failure") + ); + } + + [Fact] + public void TryCreate_ShouldNotReplaceScheme_ForIosSafari() + { + // Arrange + var url = "https://example.com/return"; + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device: BankIdTestDevices.Mobile.Ios.Safari); // Testing iOS Safari-specific behavior + + // Assert + result.Match( + redirectUrl => { + var decodedUrl = Uri.UnescapeDataString(redirectUrl.Url); + Assert.StartsWith("https://", decodedUrl); + }, + _ => Assert.Fail("Expected success but got failure") + ); + } + + [Fact] + public void TryCreate_ShouldNotReplaceScheme_ForIosEdge() + { + // Arrange + var url = "https://example.com/return"; + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device: BankIdTestDevices.Mobile.Ios.Edge); // Testing iOS Edge-specific behavior + + // Assert + result.Match( + redirectUrl => { + var decodedUrl = Uri.UnescapeDataString(redirectUrl.Url); + Assert.StartsWith("https://", decodedUrl); + }, + _ => Assert.Fail("Expected success but got failure") + ); + } + + #endregion + + #region Android Browser Tests + + [Theory] + [InlineData(nameof(BankIdTestDevices.Mobile.Android.Chrome))] + [InlineData(nameof(BankIdTestDevices.Mobile.Android.Firefox))] + [InlineData(nameof(BankIdTestDevices.Mobile.Android.Edge))] + [InlineData(nameof(BankIdTestDevices.Mobile.Android.SamsungInternet))] + [InlineData(nameof(BankIdTestDevices.Mobile.Android.Opera))] + public void TryCreate_ShouldNotReplaceScheme_ForAndroidBrowsers(string browserName) + { + // Arrange + var url = "https://example.com/return"; + var device = browserName switch // Testing Android browser-specific behavior + { + nameof(BankIdTestDevices.Mobile.Android.Chrome) => BankIdTestDevices.Mobile.Android.Chrome, + nameof(BankIdTestDevices.Mobile.Android.Firefox) => BankIdTestDevices.Mobile.Android.Firefox, + nameof(BankIdTestDevices.Mobile.Android.Edge) => BankIdTestDevices.Mobile.Android.Edge, + nameof(BankIdTestDevices.Mobile.Android.SamsungInternet) => BankIdTestDevices.Mobile.Android.SamsungInternet, + nameof(BankIdTestDevices.Mobile.Android.Opera) => BankIdTestDevices.Mobile.Android.Opera, + _ => throw new ArgumentException($"Unknown browser: {browserName}") + }; + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device); + + // Assert + result.Match( + redirectUrl => { + var decodedUrl = Uri.UnescapeDataString(redirectUrl.Url); + Assert.StartsWith("https://", decodedUrl); + }, + _ => Assert.Fail($"Expected success for {browserName} but got failure") + ); + } + + #endregion + + #region Desktop Browser Tests + + [Theory] + [InlineData(nameof(BankIdTestDevices.Desktop.Windows_Chrome))] + [InlineData(nameof(BankIdTestDevices.Desktop.Windows_Edge))] + [InlineData(nameof(BankIdTestDevices.Desktop.Windows_Firefox))] + [InlineData(nameof(BankIdTestDevices.Desktop.MacOs_Safari))] + [InlineData(nameof(BankIdTestDevices.Desktop.MacOs_Chrome))] + public void TryCreate_ShouldNotReplaceScheme_ForDesktopBrowsers(string browserName) + { + // Arrange + var url = "https://example.com/return"; + var device = browserName switch // Testing Desktop browser-specific behavior + { + nameof(BankIdTestDevices.Desktop.Windows_Chrome) => BankIdTestDevices.Desktop.Windows_Chrome, + nameof(BankIdTestDevices.Desktop.Windows_Edge) => BankIdTestDevices.Desktop.Windows_Edge, + nameof(BankIdTestDevices.Desktop.Windows_Firefox) => BankIdTestDevices.Desktop.Windows_Firefox, + nameof(BankIdTestDevices.Desktop.MacOs_Safari) => BankIdTestDevices.Desktop.MacOs_Safari, + nameof(BankIdTestDevices.Desktop.MacOs_Chrome) => BankIdTestDevices.Desktop.MacOs_Chrome, + _ => throw new ArgumentException($"Unknown browser: {browserName}") + }; + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device); + + // Assert + result.Match( + redirectUrl => { + var decodedUrl = Uri.UnescapeDataString(redirectUrl.Url); + Assert.StartsWith("https://", decodedUrl); + }, + _ => Assert.Fail($"Expected success for {browserName} but got failure") + ); + } + + #endregion + + #region Custom Browser Configuration Tests + + [Fact] + public void TryCreate_ShouldUseCustomBrowserScheme_WhenConfigProvided() + { + // Arrange + var url = "https://example.com/return"; + var customScheme = new BrowserScheme("myapp://"); + var config = new BankIdLauncherCustomBrowserConfig( + customScheme, + BrowserMightRequireUserInteractionToLaunch.Never + ); + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config, // Testing custom config override + device: BankIdTestDevices.AnyDevice); // Device irrelevant when custom config provided + + // Assert + result.Match( + redirectUrl => { + var decodedUrl = Uri.UnescapeDataString(redirectUrl.Url); + Assert.StartsWith("myapp://", decodedUrl); + }, + _ => Assert.Fail("Expected success but got failure") + ); + } + + [Fact] + public void TryCreate_ShouldPreferCustomScheme_OverDefaultIosChromeScheme() + { + // Arrange + var url = "https://example.com/return"; + var customScheme = new BrowserScheme("customapp://"); + var config = new BankIdLauncherCustomBrowserConfig( + customScheme, + BrowserMightRequireUserInteractionToLaunch.Never + ); + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config, // Testing that custom config takes precedence + device: BankIdTestDevices.Mobile.Ios.Chrome); // Would normally use chromebrowsers:// + + // Assert + result.Match( + redirectUrl => { + var decodedUrl = Uri.UnescapeDataString(redirectUrl.Url); + Assert.StartsWith("customapp://", decodedUrl); + Assert.DoesNotContain("chromebrowser://", decodedUrl); + Assert.DoesNotContain("https://", decodedUrl); + }, + _ => Assert.Fail("Expected success but got failure") + ); + } + + [Fact] + public void TryCreate_ShouldHandleCustomSchemeWithTrailingSlashes() + { + // Arrange + var url = "https://example.com/return"; + var customScheme = new BrowserScheme("myapp://"); + var config = new BankIdLauncherCustomBrowserConfig( + customScheme, + BrowserMightRequireUserInteractionToLaunch.Never + ); + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config, // Testing BrowserScheme trimming behavior + device: BankIdTestDevices.AnyDevice); // Device irrelevant when custom config provided + + // Assert + result.Match( + redirectUrl => { + var decodedUrl = Uri.UnescapeDataString(redirectUrl.Url); + // BrowserScheme trims trailing :// so we expect clean scheme + Assert.StartsWith("myapp://", decodedUrl); + }, + _ => Assert.Fail("Expected success but got failure") + ); + } + + #endregion + + #region Case Sensitivity Tests + + [Fact] + public void TryCreate_ShouldReplaceHttps_CaseInsensitive() + { + // Arrange - mixed case https + var url = "HTTPS://www.example.com/return"; + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device: BankIdTestDevices.Mobile.Ios.Chrome); // Testing case-insensitive replacement for iOS Chrome + + // Assert + result.Match( + redirectUrl => { + var decodedUrl = Uri.UnescapeDataString(redirectUrl.Url); + Assert.Equal("chromebrowser://www.example.com/return", decodedUrl); + Assert.DoesNotContain("HTTPS://", decodedUrl); + }, + _ => Assert.Fail("Expected success but got failure") + ); + } + + #endregion + + #region Implicit Conversion Tests + + [Fact] + public void ImplicitConversion_ShouldReturnUrl_WhenBankIdRedirectUrlIsNotNull() + { + // Arrange + var url = "https://example.com/return"; + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device: BankIdTestDevices.AnyDevice); // Device doesn't affect implicit conversion + + // Act + string convertedUrl = null; + result.Match( + redirectUrl => convertedUrl = redirectUrl, // Implicit conversion to string + _ => Assert.Fail("Expected success") + ); + + // Assert + Assert.NotNull(convertedUrl); + Assert.Equal(Uri.EscapeDataString(url), convertedUrl); + } + + [Fact] + public void ToString_ShouldReturnUrl() + { + // Arrange + var url = "https://example.com/return"; + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device: BankIdTestDevices.AnyDevice); // Device doesn't affect ToString + + // Act & Assert + result.Match( + redirectUrl => { + Assert.Equal(redirectUrl.Url, redirectUrl.ToString()); + }, + _ => Assert.Fail("Expected success") + ); + } + + #endregion + + #region Real-World Scenario Tests + + [Fact] + public void TryCreate_ShouldHandleTypicalLoginUrl_WithNonce() + { + // Arrange - Typical scenario from BankID docs + var url = "https://www.example.com/login#nonce=abc123def456"; + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device: BankIdTestDevices.AnyDevice); // Device doesn't matter for URL structure test + + // Assert + result.Match( + redirectUrl => { + Assert.NotNull(redirectUrl.Url); + var decodedUrl = Uri.UnescapeDataString(redirectUrl.Url); + Assert.Contains("login", decodedUrl); + Assert.Contains("nonce=abc123def456", decodedUrl); + }, + _ => Assert.Fail("Expected success but got failure") + ); + } + + [Fact] + public void TryCreate_ShouldHandleLocalhost_WithPort() + { + // Arrange - Development scenario + var url = "https://localhost:5001/ActiveLogin/BankId/Auth/Init?returnUrl=/secure"; + + // Act + var result = BankIdRedirectUrl.TryCreate( + url, + config: null, + device: BankIdTestDevices.AnyDevice); // Device doesn't affect localhost handling + + // Assert + result.Match( + redirectUrl => { + var decodedUrl = Uri.UnescapeDataString(redirectUrl.Url); + Assert.Contains("localhost:5001", decodedUrl); + }, + _ => Assert.Fail("Expected success but got failure") + ); + } + + #endregion +} diff --git a/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdTestDevices.cs b/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdTestDevices.cs new file mode 100644 index 00000000..cccc699e --- /dev/null +++ b/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdTestDevices.cs @@ -0,0 +1,74 @@ +using ActiveLogin.Authentication.BankId.Core.SupportedDevice; + +namespace ActiveLogin.Authentication.BankId.Core.Test; + +/// +/// Shared test fixture providing predefined BankIdSupportedDevice instances +/// for use across unit tests. Covers the major browsers and platforms documented +/// in the BankID integration guide. +/// +public static class BankIdTestDevices +{ + /// + /// Use this when the specific device doesn't matter for the test scenario. + /// Makes it clear that device selection is arbitrary and not part of what's being tested. + /// + public static BankIdSupportedDevice AnyDevice => Desktop.Windows_Edge; + + public static class Desktop + { + public static BankIdSupportedDevice Windows_Edge => + new(BankIdSupportedDeviceType.Desktop, BankIdSupportedDeviceOs.Windows, BankIdSupportedDeviceBrowser.Edge, new BankIdSupportedDeviceOsVersion(10)); + + public static BankIdSupportedDevice Windows_Chrome => + new(BankIdSupportedDeviceType.Desktop, BankIdSupportedDeviceOs.Windows, BankIdSupportedDeviceBrowser.Chrome, new BankIdSupportedDeviceOsVersion(10)); + + public static BankIdSupportedDevice Windows_Firefox => + new(BankIdSupportedDeviceType.Desktop, BankIdSupportedDeviceOs.Windows, BankIdSupportedDeviceBrowser.Firefox, new BankIdSupportedDeviceOsVersion(10)); + + public static BankIdSupportedDevice MacOs_Safari => + new(BankIdSupportedDeviceType.Desktop, BankIdSupportedDeviceOs.MacOs, BankIdSupportedDeviceBrowser.Safari, new BankIdSupportedDeviceOsVersion(10, 15)); + + public static BankIdSupportedDevice MacOs_Chrome => + new(BankIdSupportedDeviceType.Desktop, BankIdSupportedDeviceOs.MacOs, BankIdSupportedDeviceBrowser.Chrome, new BankIdSupportedDeviceOsVersion(10, 15)); + } + + public static class Mobile + { + public static class Android + { + public static BankIdSupportedDevice Chrome => + new(BankIdSupportedDeviceType.Mobile, BankIdSupportedDeviceOs.Android, BankIdSupportedDeviceBrowser.Chrome, new BankIdSupportedDeviceOsVersion(10)); + + public static BankIdSupportedDevice Edge => + new(BankIdSupportedDeviceType.Mobile, BankIdSupportedDeviceOs.Android, BankIdSupportedDeviceBrowser.Edge, new BankIdSupportedDeviceOsVersion(10)); + + public static BankIdSupportedDevice SamsungInternet => + new(BankIdSupportedDeviceType.Mobile, BankIdSupportedDeviceOs.Android, BankIdSupportedDeviceBrowser.SamsungBrowser, new BankIdSupportedDeviceOsVersion(10)); + + public static BankIdSupportedDevice Firefox => + new(BankIdSupportedDeviceType.Mobile, BankIdSupportedDeviceOs.Android, BankIdSupportedDeviceBrowser.Firefox, new BankIdSupportedDeviceOsVersion(10)); + + public static BankIdSupportedDevice Opera => + new(BankIdSupportedDeviceType.Mobile, BankIdSupportedDeviceOs.Android, BankIdSupportedDeviceBrowser.Opera, new BankIdSupportedDeviceOsVersion(10)); + } + + public static class Ios + { + public static BankIdSupportedDevice Safari => + new(BankIdSupportedDeviceType.Mobile, BankIdSupportedDeviceOs.Ios, BankIdSupportedDeviceBrowser.Safari, new BankIdSupportedDeviceOsVersion(14)); + + public static BankIdSupportedDevice Chrome => + new(BankIdSupportedDeviceType.Mobile, BankIdSupportedDeviceOs.Ios, BankIdSupportedDeviceBrowser.Chrome, new BankIdSupportedDeviceOsVersion(14)); + + public static BankIdSupportedDevice Firefox => + new(BankIdSupportedDeviceType.Mobile, BankIdSupportedDeviceOs.Ios, BankIdSupportedDeviceBrowser.Firefox, new BankIdSupportedDeviceOsVersion(14)); + + public static BankIdSupportedDevice Edge => + new(BankIdSupportedDeviceType.Mobile, BankIdSupportedDeviceOs.Ios, BankIdSupportedDeviceBrowser.Edge, new BankIdSupportedDeviceOsVersion(14)); + + public static BankIdSupportedDevice Opera => + new(BankIdSupportedDeviceType.Mobile, BankIdSupportedDeviceOs.Ios, BankIdSupportedDeviceBrowser.Opera, new BankIdSupportedDeviceOsVersion(14)); + } + } +} diff --git a/test/ActiveLogin.Authentication.BankId.Core.Test/Helpers/TestBankIdLauncher.cs b/test/ActiveLogin.Authentication.BankId.Core.Test/Helpers/TestBankIdLauncher.cs index 33cbfce6..8c6ead9a 100644 --- a/test/ActiveLogin.Authentication.BankId.Core.Test/Helpers/TestBankIdLauncher.cs +++ b/test/ActiveLogin.Authentication.BankId.Core.Test/Helpers/TestBankIdLauncher.cs @@ -9,6 +9,6 @@ internal class TestBankIdLauncher : IBankIdLauncher public Task GetLaunchInfoAsync(LaunchUrlRequest request) { // Always redirect back without user interaction in simulated mode - return Task.FromResult(new BankIdLaunchInfo(request.RedirectUrl, false, false, request.RedirectUrl)); + return Task.FromResult(new BankIdLaunchInfo(request.RedirectUrl, false, false)); } }