Skip to content

Latest commit

 

History

History
591 lines (451 loc) · 22.1 KB

getting-started-bankid.md

File metadata and controls

591 lines (451 loc) · 22.1 KB

Getting started with BankID

Dependencies

The BankID packages has UI that uses classes from Bootstrap 4, please make sure these styles are available on the page for the expected UI.

Preparation

BankID requires you to use a client certificate and trust a specific root CA-certificate.

  1. Read through the BankID Relying Party Guidelines. This ensures you have a basic understanding of the terminology as well as how the flow and security works.
  2. Download the SSL certificate for test (FPTestcert2.pfx).
  3. Contact a reseller to get your very own client certificate for production. This will probably take a few business days to get sorted. Please ask for "Direktupphandlad BankID" as they otherwise might refer you to GrandID.
  4. The root CA-certificates specified in BankID Relying Party Guidelines (#7 for Production and #8 for Test environment) needs to be trusted at the computer where the app will run. Save those certificates as BankIdRootCertificate-Prod.crt and BankIdRootCertificate-Test.crt.
    1. If running in Azure App Service, where trusting custom certificates is not supported, there are extensions to handle that scenario. Instead of trusting the certificate, place it in your web project and make sure CopyToOutputDirectory is set to Always.
    2. Add the following configuration values. The FilePath should point to the certificate you just added, for example:
{
    "ActiveLogin:BankId:CaCertificate:FilePath": "Certificates\\BankIdRootCertificate-[Test or Prod].crt"
}

Note: When using MacOS or Linux, path strings use '/' for subfolders: "Certificates/BankIdRootCertificate-[Test or Prod].crt"

Storing certificates in Azure

These are only necessary if you plan to store your certificates in Azure KeyVault (recommended) and use the extension for easy integration with BankID.

Deploy to Azure

  1. Deploy Azure KeyVault to your subscription. The ARM-template available in AzureProvisioningSample contains configuration that creates a KeyVault and enables Managed Service Identity for the App Service.
  2. Import the certificates to your Azure Key Vault.
  3. Add the following to your config, the secret identifier and auth settings.
{
    "ActiveLogin:BankId:ClientCertificate": {
        "UseManagedIdentity": true,

        "AzureAdTenantId": "",
        "AzureAdClientId": "",
        "AzureAdClientSecret": "",

        "AzureKeyVaultUri": "TODO-ADD-YOUR-VALUE",
        "AzureKeyVaultSecretName": "TODO-ADD-YOUR-VALUE"
    }
}

Certificates are secrets

When configuring the AzureKeyVaultSecretName, the name is retrieved from the Certificates rather than Secrets in the Azure Portal. It is called a secret in the API since this is how Azure Key Vault exposes certificates with private keys.

You can read more about the reasoning behind this in this blog post or in the very extensive official documentation.

Authentication using Managed Identity

If possible, you should use Azure AD Managed Identity to connect to Azure KeyVault. To use Managed Identity, set UseManagedIdentity to true.

Authentication using Client Credentials

If Managed Identity can't be used, you can authenticate to Azure Key Vault using Client Credentials. Then set UseManagedIdentity to false and instead set values for AzureAdTenantId, AzureAdClientId and AzureAdClientSecret.

Environments

Simulated environment

For trying out quickly (without the need of certificates) you can use an in-memory implementation of the API by using .UseSimulatedEnvironment(). This could also be good when writing tests.

Simulated environment with no config

services
    .AddAuthentication()
    .AddBankId(builder =>
    {
        builder
            .UseSimulatedEnvironment()
            .AddSameDevice()
            .AddOtherDevice()
            .UseQrCoderQrCodeGenerator();
    })

Development environment with custom person info

The faked name and personal identity number can also be customized like this.

services
    .AddAuthentication()
    .AddBankId(builder =>
    {
        builder
            .UseSimulatedEnvironment("Alice", "Smith", "199908072391")
            .AddSameDevice()
            .AddOtherDevice()
            .UseQrCoderQrCodeGenerator();
    });

Production environment

This will use the real REST API for BankID, connecting to either the Test or Production environment. It requires you to have the certificates described under Preparation above.

services.AddAuthentication()
        .AddBankId(builder =>
    {
        builder
            .UseProductionEnvironment()
            ...
    });

Test

These samples uses the production environment, to use the test environment, simply swap .UseProductionEnvironment() with .UseTestEnvironment(). You will also have to use a different client and root certificate, see info under Preparation above.

services.AddAuthentication()
        .AddBankId(builder =>
    {
        builder
            .UseTestEnvironment()
            ...
    });

Samples

Using client certificate from Azure KeyVault

services.AddAuthentication()
        .AddBankId(builder =>
    {
        builder
            .UseProductionEnvironment()
            .UseClientCertificateFromAzureKeyVault(Configuration.GetSection("ActiveLogin:BankId:ClientCertificate"))
            ...
    });

Using client certificate from custom source

services.AddAuthentication()
        .AddBankId(builder =>
    {
        builder
            .UseProductionEnvironment()
            .UseClientCertificate(() => new X509Certificate2( ... ))
            ...
    });

Using root ca certificate

BankID uses a self signed root ca certificate that you need to trust. This is not possible in all scenarios, like in Azure App Service. To solve this there is an extension available to trust a custom root certificate using code. It can be used like this.

services.AddAuthentication()
        .AddBankId(builder =>
    {
        builder
            .UseProductionEnvironment()
            .UseRootCaCertificate(Path.Combine(_environment.ContentRootPath, Configuration.GetValue<string>("ActiveLogin:BankId:CaCertificate:FilePath")))
            ...
    });

Adding schemas

  • Same device: Launches the BankID app on the same device, no need to enter any personal identity number.
  • Other device: You enter your personal identity number and can manually launch the app on your smartphone.
services
    .AddAuthentication()
    .AddBankId(builder =>
    {
        builder
            .UseProductionEnvironment()
            ...
            .AddSameDevice()
            .AddOtherDevice();
    });

Customizing schemas

By default, Add*Device will use predefined schemas and display names, but they can be changed.

services
    .AddAuthentication()
    .AddBankId(builder =>
    {
        builder
            .UseProductionEnvironment()
            ...
            .AddSameDevice("custom-auth-scheme", "Custom display name", options => { ... })
            .AddOtherDevice(BankIdDefaults.OtherDeviceAuthenticationScheme, "Custom display name", options => { ... });
    });

Custom schema

If you want to roll your own, complete custom config, that can be done using .AddCustom().

services
    .AddAuthentication()
    .AddBankId(builder =>
    {
        builder
            ...
            .AddCustom(options => {
                options.BankIdAutoLaunch = true;
                options.BankIdAllowChangingPersonalIdentityNumber = false;
            });
    });

Customizing BankID

BankId options allows you to set and override some options such as these.

.AddOtherDevice(options =>
{
    // If the user can use biometric identification such as fingerprint or face recognition
    options.BankIdAllowBiometric = false;

    // Limit possible login methods to, for example, only allow BankID on smartcard.
    options.BankIdCertificatePolicies = BankIdCertificatePolicies.GetPoliciesForProductionEnvironment(...);

    // Issue birthdate claim based on data extracted from the personal identity number
    options.IssueBirthdateClaim = true;

    // Issue gender claim based on data extracted from the personal identity number
    options.IssueGenderClaim = true;

    // Turn off qr code and use personal identity number instead
    options.BankIdUseQrCode = false;
});

If you want to apply some options for all BankID schemes, you can do so by using .Configure(...).

.Configure(options =>
{
    options.IssueBirthdateClaim = true;
    options.IssueGenderClaim = true;
});

Setting the return URL for cancellation

The defaults for cancellation are as follows:

  • Same Device Scheme returns to scheme selection
  • Other Device Scheme returns to scheme selection when using QR codes
  • Other Device Scheme returns to PIN input when using PIN input instead of QR codes

It is possible to override the default navigation when cancelling an authentication request. The URL used for navigation is set through the cancelReturnUrl item in the AuthenticationProperties passed in the authentication challenge.

var props = new AuthenticationProperties
{
    RedirectUri = Url.Action(nameof(ExternalLoginCallback)),
    Items =
    {
        { "returnUrl", "~/" },
        { "cancelReturnUrl", "~/some-custom-cancellation-url" },
        { "scheme", provider }
    }
};

return Challenge(props, provider);

Event listeners

During the login flow, quite a lot of things are happening and using our event listeners you can listen and act on those events. By implementing and regestering IBankIdEventListener you will be notified when an event occurs. A common scenario is logging.

BankIdEvent is the base class for all events which all events will inherit from. Each event might (and in most cases will) have unique properties relevant for that specific event.

Event types

At the moment, we trigger these events:

  • AspNet
    • BankIdAspNetChallengeSuccessEvent
    • BankIdAspNetAuthenticateSuccessEvent
    • BankIdAspNetAuthenticateFailureEvent
  • Auth
    • BankIdAuthSuccessEvent
    • BankIdAuthErrorEvent
  • Collect
    • BankIdCollectPendingEvent
    • BankIdCollectCompletedEvent
    • BankIdCollectFailureEvent
    • BankIdCollectErrorEvent
  • Cancel
    • BankIdCancelSuccessEvent
    • BankIdCancelErrorEvent

Sample implementation

public class BankIdSampleEventListener : IBankIdEventListener
{
    public Task HandleAsync(BankIdEvent bankIdEvent)
    {
        Console.WriteLine($"{bankIdEvent.EventTypeName}: {bankIdEvent.EventSeverity}");
        return Task.CompletedTask;
    }
}

services
    .AddAuthentication()
    .AddBankId(builder =>
    {
        builder
            //...
            .AddEventListener<BankIdSampleEventListener>();
    });

Supported event listeners

BankIdDebugEventListener

BankIdDebugEventListener will listen for all events and write them as serialized JSON to the debug log using ILogger.LogDebug(...). Call builder.AddDebugEventListener() to enable it. Good to have for local development to see all details about what is happening.

services
    .AddAuthentication()
    .AddBankId(builder =>
    {
        builder
            //...
            .AddDebugEventListener();
    });
BankIdApplicationInsightsEventListener

BankIdApplicationInsightsEventListener will listen for all events and write them to Application Insights. Call builder.AddApplicationInsightsEventListener() to enable it. Note that you can supply options to enable logging of metadata, such as personal identity number, age and IP.

Note: This event listener is available is available through a separate package called ActiveLogin.Authentication.BankId.AspNetCore.AzureMonitor.

services
    .AddAuthentication()
    .AddBankId(builder =>
    {
        builder
            //...
            .AddApplicationInsightsEventListener();
    });
BankIdLoggerEventListener

BankIdDebugEventListener will listen for all events and write them with a descriptive text to the log using ILogger.Log(...). This listener is registered by default on startup, se info below if you want to clear the default listeners.

services
    .AddAuthentication()
    .AddBankId(builder =>
    {
        builder
            //...
            .AddDebugEventListener();
    });

Default registered event listeners

By default, two event listeners will be enabled:

  • BankIdLoggerEventListener (Log all events to ILogger)
  • BankIdResultStoreEventListener (Map the completion event for IBankIdResultStore, see info below under Store data on auth completion.)

If you want to remove those implementations, remove any class implementing IBankIdEventListener from the ASP.NET Core services in your Startup.cs:

services.RemoveAll(typeof(IBankIdEventListener));

Store data on auth completion

When the login flow is completed and the collect request to BankID returns data, any class implementing IBankIdResultStore registered in the DI will be called. There is a shorthand method (AddResultStore) on the BankIdBuilder to register the implementation.

Note: IBankIdResultStore is just a shorthand for the BankIdCollectCompletedEvent as described above.

Sample implementation:

public class BankIdResultSampleLoggerStore : IBankIdResultStore
{
    private readonly EventId _eventId = new EventId(101, "StoreCollectCompletedCompletionData");
    private readonly ILogger<BankIdResultTraceLoggerStore> _logger;

    public BankIdResultSampleLoggerStore(ILogger<BankIdResultTraceLoggerStore> logger)
    {
        _logger = logger;
    }

    public Task StoreCollectCompletedCompletionData(string orderRef, CompletionData completionData)
    {
        _logger.LogTrace(_eventId, "Storing completion data for OrderRef '{OrderRef}' (UserPersonalIdentityNumber: '{UserPersonalIdentityNumber}')", orderRef, completionData.User.PersonalIdentityNumber);

        return Task.CompletedTask;
    }
}

services
    .AddAuthentication()
    .AddBankId(builder =>
    {
        builder
            //...
            .AddResultStore<BankIdResultSampleLoggerStore>();
    });

The default implementation will log all data to the tracelog. If you want to remove that implementation, remove any class implementing IBankIdResultStore from the ASP.NET Core services in your Startup.cs:

services.RemoveAll(typeof(IBankIdResultStore));

Resolve the end user ip

In some scenarios, like running behind a proxy, you might want to resolve the end user IP yourself and override the default implementaion.

Either register a class implementing IEndUserIpResolver:

builder.UseEndUserIpResolver<EndUserIpResolver>();

Or use the shorthand version:

builder.UseEndUserIpResolver(httpContext =>
{
    return httpContext.Connection.RemoteIpAddress.ToString();
});

Full sample for production

Finally, a full sample on how to use BankID in production with client certificate from Azure KeyVault and trusting a custom root certificate.

services
    .AddAuthentication()
    .AddBankId(builder =>
    {
        builder
            .UseProductionEnvironment()
            .UseClientCertificateFromAzureKeyVault(Configuration.GetSection("ActiveLogin:BankId:ClientCertificate"))
            .UseRootCaCertificate(Path.Combine(_environment.ContentRootPath, Configuration.GetValue<string>("ActiveLogin:BankId:CaCertificate:FilePath")))
            .AddSameDevice(options => { })
            .AddOtherDevice(options => { })
            .UseQrCoderQrCodeGenerator();
    });

Custom QR code generation

By default the ActiveLogin.Authentication.BankId.AspNetCore.Qr package is needed to generate QR codes using the UseQrCoderQrCodeGenerator extension method.

If you wish to provide your own implementation of QR code generation simply implement the IBankIdQrCodeGenerator interface and add your implementation as a service.

services.AddTransient<IBankIdQrCodeGenerator, CustomQrCodeGenerator>();

BankID Certificate Policies

BankId options allows you to set a list of certificate policies and there is a class available to help you out with this.

.AddOtherDevice(options =>
{
	options.BankIdCertificatePolicies = BankIdCertificatePolicies.GetPoliciesForProductionEnvironment(BankIdCertificatePolicy.BankIdOnFile, BankIdCertificatePolicy.MobileBankId);
});

Because the policies have different values for test and production environment, you need to use either .GetPoliciesForProductionEnvironment() or .GetPoliciesForTestEnvironment() depending on what environment you are using.

Example:

.AddOtherDevice(options =>
{
	var policies = new[] { BankIdCertificatePolicy.BankIdOnFile, BankIdCertificatePolicy.MobileBankId };
	if(isProductionEnvironment) {
		options.BankIdCertificatePolicies = BankIdCertificatePolicies.GetPoliciesForProductionEnvironment(policies);
	} else {
		options.BankIdCertificatePolicies = BankIdCertificatePolicies.GetPoliciesForTestEnvironment(policies);
	}
});

Multi tenant scenario

With the current architecture of Active Login all services are registered "globally" and you can't call .AddBankId() more than once. To run Active Login in a multi tenant scenario, where different customers should use different certificates, you could register multiple certificates and on runtime select the correct one per request. With our current solution, this requires you to disable pooling of the SocketsHttpHandler so we've decided not to ship that code in the NuGet-package, but below you'll find a sample on how it could be configured. We hope to redesign this in the future.

Note: The code below is a sample and because it disables PooledConnection it might (and will) have performance implications.

internal static class BankIdBuilderExtensions
{
    public static IBankIdBuilder UseClientCertificateResolver(this IBankIdBuilder builder, Func<ServiceProvider, X509CertificateCollection, string, X509Certificate> configureClientCertificateResolver)
    {
        builder.ConfigureHttpClientHandler(httpClientHandler =>
        {
            var services = builder.AuthenticationBuilder.Services;
            var serviceProvider = services.BuildServiceProvider();

            httpClientHandler.PooledConnectionLifetime = TimeSpan.Zero;
            httpClientHandler.SslOptions.LocalCertificateSelectionCallback =
                (sender, host, certificates, certificate, issuers) => configureClientCertificateResolver(serviceProvider, certificates, host);
        });

        return builder;
    }
}

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...
        services.AddAuthentication()
            .AddBankId(builder =>
            {
                builder
                    .UseClientCertificateFromAzureKeyVault(Configuration.GetSection("ActiveLogin:BankId:ClientCertificate1"))
                    .UseClientCertificateFromAzureKeyVault(Configuration.GetSection("ActiveLogin:BankId:ClientCertificate2"))
                    .UseClientCertificateFromAzureKeyVault(Configuration.GetSection("ActiveLogin:BankId:ClientCertificate3"))
                    .UseClientCertificateResolver((serviceCollection, certificates, hostname) =>
                    {
                        // Apply logic here to select the correct certificate
                        return certificates[0];
                    });

                // ...
            }
    }
}

FAQ

How do I customize the UI?

The UI is bundled into the package as a Razor Class Library, a technique that allows to override the parts you want to customize. The Views and Controllers that can be customized can be found in the GitHub repo.

Can the messages be localized?

The messages are already localized to English and Swedish using the official recommended texts. To select what texts that are used you can for example use the localization middleware in ASP.NET Core.

How can the certificates be handled when running in Linux?

X509Certificate2 can not be handled in the same way when running in Linux as on Windows. The certificate is Base64 encoded and must be decoded before creating the X509Certificate2 instance. Below is an example for the BankId root certificate:

Copy the content between Begin certificate and End certificate, and paste it into a resource string in a Resource.resx file.

With the certificate in the reosurce, this code can be used to create the X509Certificate2 instance. Note the second line that decodes the Base64 string.

var rootCertEncoded = CertificateResources.BankIdRootTestCertificate;
var rootCertBytes = Convert.FromBase64String(rootCertEncoded);

return new X509Certificate2(rootCertBytes, string.Empty, X509KeyStorageFlags.MachineKeySet);