diff --git a/docs/articles/bankid.md b/docs/articles/bankid.md index 7919c1ed..9eacc2af 100644 --- a/docs/articles/bankid.md +++ b/docs/articles/bankid.md @@ -1394,6 +1394,21 @@ services.AddTransient(); The functionality provided tries to detect the device by looking at the user agent. We need to know what device is used to launch the BankId app and this differs from iOS/Android/PC/Mac. +#### How the BankID app is launched + +By default, Active Login follows [BankID's autostart guidance](https://developers.bankid.com/how-to-guides/autostart): + +* **Mobile (iOS and Android):** the app is launched using the universal/app link `https://app.bankid.com/`. +* **Desktop:** the app is launched using the custom scheme `bankid:///`. + +On Android, **Firefox and Samsung Internet are exceptions**. Our browser tests show that the BankID app is not launched when using the universal/app link in these browsers. Active Login therefore falls back to the custom `bankid:///` scheme for these browsers. All other supported Android browsers use the universal/app link. + +The launch link is opened from the client using an anchor element with `referrerPolicy="origin"`, as recommended by BankID. + +Autostart is the default. A manual *"Start the BankID app"* button is shown as a fallback on Android browsers that block launching a third-party app without a user gesture. + +The deprecated `redirect` parameter is still included in the launch URL for backwards compatibility. BankID now recommends providing the return URL in the backend call when creating the order instead, see [Return URL](https://developers.bankid.com/how-to-guides/return-url). + By implementing `IBankIdLauncher` you can customize exactly how to launch the app. It is very rare that you need to change this, but could be relevant if you use Active Login for authenticating a user in a native mobile app. ```csharp @@ -1683,16 +1698,16 @@ All browsers on mobile are supported to show the UI, but the redirect flow has b - Chrome - Edge - Firefox - - Opera Touch - Android - Chrome - Firefox - Edge - Samsung Internet - - Opera Mini -___Note:___ Brave on iOS/Android identifies as Safari or Chrome for privacy reasons and will get wrong configuration, so the redirect flow will fail. +___Note:___ On mobile, the BankID app is launched using the App Link `https://app.bankid.com/` by default, in accordance with [BankID's autostart guidance](https://developers.bankid.com/how-to-guides/autostart). On Android, Firefox and Samsung Internet are exceptions because our tests show that the App Link does not launch the BankID app in these browsers. For these browsers, Active Login falls back to the `bankid:///` custom scheme. -___Note:___ If you aim to support IE11 a polyfill for some JavaScript features we are using is needed. +Autostart is enabled by default. A manual launch button is shown as a fallback on Android browsers that require a user gesture to launch a third-party app. + +___Note:___ Brave on iOS/Android identifies as Safari or Chrome for privacy reasons and will get wrong configuration, so the redirect flow will fail. * [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API): https://github.com/github/fetch diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/Client/activelogin-main.ts b/src/ActiveLogin.Authentication.BankId.AspNetCore/Client/activelogin-main.ts index 1439fd20..6be2d60d 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/Client/activelogin-main.ts +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/Client/activelogin-main.ts @@ -116,6 +116,15 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: var flowIsCancelledByUser = false; var flowIsFinished = false; + function launchBankIdApp(url: string) { + // Use BankID recommended approach for launching the app + // See: https://developers.bankid.com/getting-started/autostart + const link = document.createElement("a"); + link.href = url; + link.referrerPolicy = "origin"; + link.click(); + } + function enableCancelButton(requestVerificationToken: string, cancelUrl: string, orderRef: string = null) { var onCancelButtonClick = (event: Event) => { cancel(requestVerificationToken, cancelUrl, orderRef); @@ -139,7 +148,7 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: if (data.deviceMightRequireUserInteractionToLaunchBankIdApp) { var startBankIdAppButtonOnClick = (event: Event) => { - window.location.href = data.redirectUri; + launchBankIdApp(data.redirectUri); hide(startBankIdAppButtonElement); event.target.removeEventListener("click", startBankIdAppButtonOnClick); }; @@ -147,7 +156,7 @@ function activeloginInit(configuration: IBankIdUiScriptConfiguration, initState: show(startBankIdAppButtonElement); } else { - window.location.href = data.redirectUri; + launchBankIdApp(data.redirectUri); } } diff --git a/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncher.cs b/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncher.cs index 6e1c6075..367d3228 100644 --- a/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncher.cs +++ b/src/ActiveLogin.Authentication.BankId.Core/Launcher/BankIdLauncher.cs @@ -47,14 +47,18 @@ public async Task GetLaunchInfoAsync(LaunchUrlRequest request) private bool GetDeviceMightRequireUserInteractionToLaunchBankIdApp(BankIdSupportedDevice detectedDevice, BankIdLauncherCustomBrowserConfig? customBrowserConfig) { var userInteractionBehaviour = customBrowserConfig?.BrowserMightRequireUserInteractionToLaunch ?? BrowserMightRequireUserInteractionToLaunch.Default; - + return userInteractionBehaviour switch { BrowserMightRequireUserInteractionToLaunch.Always => true, BrowserMightRequireUserInteractionToLaunch.Never => false, - // On Android, some browsers will (for security reasons) not launching a - // third party app/scheme (BankID) if there is no user interaction. + // BankID recommends autostart as the default, with a manual button only as + // a fallback when autostart can't work (https://developers.bankid.com/how-to-guides/autostart). + // + // On Android, some browsers will (for security reasons) not launch a + // third party app/scheme (BankID) without user interaction, so autostart + // is skipped in favour of the fallback button there. // // - Chrome, Edge, Samsung Internet Browser and Brave is confirmed to require User Interaction // - Firefox and Opera is confirmed to work without User Interaction @@ -99,20 +103,18 @@ private string GetPrefixPart(BankIdSupportedDevice device) private static bool CanUseAppLink(BankIdSupportedDevice device) { - // Only Safari on IOS and Chrome or Edge on Android version >= 6 seems to support - // the https://app.bankid.com/ launch url + // Universal Links / App Links (https://app.bankid.com/) are the + // recommended approach for mobile devices according to BankID: + // https://developers.bankid.com/how-to-guides/autostart + // + // Our Android browser tests show that the App Link does not launch + // BankID in Firefox and Samsung Internet. These browsers therefore + // fall back to the bankid:// scheme. - return device is - { - DeviceOs: BankIdSupportedDeviceOs.Ios, - DeviceBrowser: BankIdSupportedDeviceBrowser.Safari - } - or - { - DeviceOs: BankIdSupportedDeviceOs.Android, - DeviceOsVersion.MajorVersion: >= 6, - DeviceBrowser: BankIdSupportedDeviceBrowser.Chrome or BankIdSupportedDeviceBrowser.Edge - }; + return device.DeviceOs == BankIdSupportedDeviceOs.Ios + || (device.DeviceOs == BankIdSupportedDeviceOs.Android + && device.DeviceBrowser != BankIdSupportedDeviceBrowser.Firefox + && device.DeviceBrowser != BankIdSupportedDeviceBrowser.SamsungBrowser); } private string GetQueryStringPart(BankIdSupportedDevice device, LaunchUrlRequest request, BankIdLauncherCustomBrowserConfig? customBrowserConfig) diff --git a/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdLauncher_Tests.cs b/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdLauncher_Tests.cs index 1c02718f..c145466c 100644 --- a/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdLauncher_Tests.cs +++ b/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdLauncher_Tests.cs @@ -1,3 +1,4 @@ +using System.Text.Encodings.Web; using System.Threading.Tasks; using ActiveLogin.Authentication.BankId.Core.Launcher; @@ -33,6 +34,189 @@ public async Task BankIdLauncher_Should_UseReloadBehaviourWhenImplemented() Assert.True(info.DeviceWillReloadPageOnReturnFromBankIdApp); } + [Theory] + [InlineData(BankIdSupportedDeviceOs.Ios, BankIdSupportedDeviceBrowser.Safari)] + [InlineData(BankIdSupportedDeviceOs.Ios, BankIdSupportedDeviceBrowser.Chrome)] + [InlineData(BankIdSupportedDeviceOs.Ios, BankIdSupportedDeviceBrowser.Edge)] + [InlineData(BankIdSupportedDeviceOs.Ios, BankIdSupportedDeviceBrowser.Firefox)] + [InlineData(BankIdSupportedDeviceOs.Ios, BankIdSupportedDeviceBrowser.Opera)] + [InlineData(BankIdSupportedDeviceOs.Android, BankIdSupportedDeviceBrowser.Chrome)] + [InlineData(BankIdSupportedDeviceOs.Android, BankIdSupportedDeviceBrowser.Edge)] + [InlineData(BankIdSupportedDeviceOs.Android, BankIdSupportedDeviceBrowser.Opera)] + public async Task BankIdLauncher_Should_UseAppLink_ForSupportedMobileDevices(BankIdSupportedDeviceOs os, BankIdSupportedDeviceBrowser browser) + { + var launcher = CreateLauncher(Mobile(os, browser)); + + var info = await launcher.GetLaunchInfoAsync(new LaunchUrlRequest("https://example.com/return", "token")); + + Assert.StartsWith("https://app.bankid.com/", info.LaunchUrl); + } + + [Theory] + [InlineData(BankIdSupportedDeviceOs.Android, BankIdSupportedDeviceBrowser.SamsungBrowser)] + [InlineData(BankIdSupportedDeviceOs.Android, BankIdSupportedDeviceBrowser.Firefox)] + public async Task BankIdLauncher_Should_UseScheme_ForMobileDevicesNotSupportingAppLink(BankIdSupportedDeviceOs os, BankIdSupportedDeviceBrowser browser) + { + var launcher = CreateLauncher(Mobile(os, browser)); + + var info = await launcher.GetLaunchInfoAsync(new LaunchUrlRequest("https://example.com/return", "token")); + + Assert.StartsWith("bankid:///", info.LaunchUrl); + } + + [Fact] + public async Task BankIdLauncher_Should_UseScheme_ForDesktop() + { + var launcher = CreateLauncher(new BankIdSupportedDevice( + BankIdSupportedDeviceType.Desktop, + BankIdSupportedDeviceOs.Windows, + BankIdSupportedDeviceBrowser.Chrome, + BankIdSupportedDeviceOsVersion.Empty)); + + var info = await launcher.GetLaunchInfoAsync(new LaunchUrlRequest("https://example.com/return", "token")); + + Assert.StartsWith("bankid:///", info.LaunchUrl); + } + + [Fact] + public async Task BankIdLauncher_Should_KeepAutostart_ForIos() + { + var launcher = CreateLauncher(Mobile(BankIdSupportedDeviceOs.Ios, BankIdSupportedDeviceBrowser.Safari)); + + var info = await launcher.GetLaunchInfoAsync(new LaunchUrlRequest("https://example.com/return", "token")); + + Assert.False(info.DeviceMightRequireUserInteractionToLaunchBankIdApp); + } + + [Theory] + [InlineData(BankIdSupportedDeviceBrowser.Chrome, true)] + [InlineData(BankIdSupportedDeviceBrowser.Edge, true)] + [InlineData(BankIdSupportedDeviceBrowser.SamsungBrowser, true)] + [InlineData(BankIdSupportedDeviceBrowser.Firefox, false)] + [InlineData(BankIdSupportedDeviceBrowser.Opera, false)] + public async Task BankIdLauncher_Should_RequireUserInteraction_OnlyForRestrictedAndroidBrowsers(BankIdSupportedDeviceBrowser browser, bool expected) + { + var launcher = CreateLauncher(Mobile(BankIdSupportedDeviceOs.Android, browser)); + + var info = await launcher.GetLaunchInfoAsync(new LaunchUrlRequest("https://example.com/return", "token")); + + Assert.Equal(expected, info.DeviceMightRequireUserInteractionToLaunchBankIdApp); + } + + [Theory] + [InlineData(BrowserMightRequireUserInteractionToLaunch.Always, true)] + [InlineData(BrowserMightRequireUserInteractionToLaunch.Never, false)] + public async Task BankIdLauncher_Should_HonorCustomBrowserInteractionOverride(BrowserMightRequireUserInteractionToLaunch behaviour, bool expected) + { + // Use an iOS device where the default would be 'false' to prove the override is applied. + var launcher = new BankIdLauncher( + new ConfigurableDeviceDetector(Mobile(BankIdSupportedDeviceOs.Ios, BankIdSupportedDeviceBrowser.Safari)), + new[] { new InteractionOverrideCustomBrowser(behaviour) }); + + var info = await launcher.GetLaunchInfoAsync(new LaunchUrlRequest("https://example.com/return", "token")); + + Assert.Equal(expected, info.DeviceMightRequireUserInteractionToLaunchBankIdApp); + } + + [Fact] + public async Task BankIdLauncher_Should_SetRedirectNull_ForAndroid() + { + // BankID guideline: Android app links should use redirect=null. + var launcher = CreateLauncher(Mobile(BankIdSupportedDeviceOs.Android, BankIdSupportedDeviceBrowser.Chrome)); + + var info = await launcher.GetLaunchInfoAsync(new LaunchUrlRequest("https://example.com/return", "token")); + + Assert.Contains("redirect=null", info.LaunchUrl); + } + + [Fact] + public async Task BankIdLauncher_Should_UseReturnUrlAsRedirect_ForIosSafari() + { + const string returnUrl = "https://example.com/return"; + var launcher = CreateLauncher(Mobile(BankIdSupportedDeviceOs.Ios, BankIdSupportedDeviceBrowser.Safari)); + + var info = await launcher.GetLaunchInfoAsync(new LaunchUrlRequest(returnUrl, "token")); + + Assert.Contains($"redirect={UrlEncoder.Default.Encode(returnUrl)}", info.LaunchUrl); + } + + [Theory] + [InlineData(BankIdSupportedDeviceBrowser.Chrome, "googlechromes://")] + [InlineData(BankIdSupportedDeviceBrowser.Firefox, "firefox://")] + public async Task BankIdLauncher_Should_UseBrowserSchemeAsRedirect_ForIosThirdPartyBrowsers(BankIdSupportedDeviceBrowser browser, string expectedScheme) + { + var launcher = CreateLauncher(Mobile(BankIdSupportedDeviceOs.Ios, browser)); + + var info = await launcher.GetLaunchInfoAsync(new LaunchUrlRequest("https://example.com/return", "token")); + + Assert.Contains($"redirect={UrlEncoder.Default.Encode(expectedScheme)}", info.LaunchUrl); + } + + [Theory] + [InlineData(BankIdSupportedDeviceBrowser.Edge)] + [InlineData(BankIdSupportedDeviceBrowser.Opera)] + public async Task BankIdLauncher_Should_SetRedirectNull_ForIosEdgeAndOpera(BankIdSupportedDeviceBrowser browser) + { + var launcher = CreateLauncher(Mobile(BankIdSupportedDeviceOs.Ios, browser)); + + var info = await launcher.GetLaunchInfoAsync(new LaunchUrlRequest("https://example.com/return", "token")); + + Assert.Contains("redirect=null", info.LaunchUrl); + } + + private static BankIdLauncher CreateLauncher(BankIdSupportedDevice device) + { + return new BankIdLauncher( + new ConfigurableDeviceDetector(device), + System.Array.Empty()); + } + + private static BankIdSupportedDevice Mobile(BankIdSupportedDeviceOs os, BankIdSupportedDeviceBrowser browser) + { + return new BankIdSupportedDevice( + BankIdSupportedDeviceType.Mobile, + os, + browser, + new BankIdSupportedDeviceOsVersion(15)); + } + + private class InteractionOverrideCustomBrowser : IBankIdLauncherCustomBrowser + { + private readonly BrowserMightRequireUserInteractionToLaunch _behaviour; + + public InteractionOverrideCustomBrowser(BrowserMightRequireUserInteractionToLaunch behaviour) + { + _behaviour = behaviour; + } + + public Task IsApplicable(BankIdLauncherCustomBrowserContext context) + { + return Task.FromResult(true); + } + + public Task GetCustomAppCallbackResult(BankIdLauncherCustomBrowserContext context) + { + return Task.FromResult( + new BankIdLauncherCustomBrowserConfig(null, BrowserReloadBehaviourOnReturnFromBankIdApp.Default, _behaviour) + ); + } + } + + private class ConfigurableDeviceDetector : IBankIdSupportedDeviceDetector + { + private readonly BankIdSupportedDevice _device; + + public ConfigurableDeviceDetector(BankIdSupportedDevice device) + { + _device = device; + } + + public BankIdSupportedDevice Detect() + { + return _device; + } + } + private class TestBankIdLauncherCustomBrowser : IBankIdLauncherCustomBrowser { public Task IsApplicable(BankIdLauncherCustomBrowserContext context)