Authentication using Swedish BankID

More information

You can find more information on BankID here: https://www.bankid.com/en/om-bankid/bankid-in-my-services - to use it outside testing, you will need to sign an agreement with a selling BankID Bank - see details at https://www.bankid.com

Configuration

You can enable authentication using the Swedish BankID by enabling the authentication plugin:

dk.itp.security.authentication.bankid.se.BankIDSEAuthenticationPlugin

You do this by adding it to the session controllers configuration, to the authentication plugins property.

Once added, you need to provide additional configuration to it, here is an example that works with the test environment:

<group name="BankID SE" description="Swedish BankID">
	<property name="bankid.se.acceptedsslcerts" value="${portalprotect.home}/config/x509/bankid/servercert_test.cer" description="Valid server SSL certificate CA"/>
	<property name="bankid.se.keystore.file" value="${portalprotect.home}/config/x509/bankid/FPTestcert2_20150818_102329.pfx" description="Keystore containing SSL client certificate and private key"/>
	<property name="bankid.se.keystore.password" value="qwerty123" description="Filename containing GeoIP ISP database"/>
	<property name="bankid.se.url" value="https://appapi2.test.bankid.com/rp/v4" description="URL to BankID Server"/>
</group>

Below, is the complete list of possible configuration entries:

PropertyDefault valueDescription
bankid.se.url
URL for the BankID Servers
bankid.se.keystore.providerBCName of JCE provider to use when loading SSL client certificate keystore
bankid.se.keystore.typePKCS12Keystore type, usually PKCS12, JKS or luna
bankid.se.keystore.file
Name of keystore file
bankid.se.keystore.password
Password to keystore, can optionally be encrypted/obfuscated - see Encrypting or Obfuscating Passwords
bankid.se.keystore.privkeyalias
Alias of private key to use - if not specified, first key in the keystore is used
bankid.se.keystore.certalias
Certificate Alias
bankid.se.sslprovidername
Name of SSL provider if the default in the JVM should not be used
bankid.se.ssl.protocolTLSv1.2SSL protocol to use
bankid.se.ssl.verifycerttrueSet to false to disable SSL server certificate
bankid.se.ssl.verifyhostnametrueSet to false to disable verification that hostname matches certificate
bankid.se.acceptedsslcerts
List of trusted SSL server signing certificates
bankid.se.ignoreipaddressfalseIf set to true, clients IP address is not send to BankID - in test, we have seen BankID return an
http.proxyHost
Proxy host to use when sending requests
http.proxyPort
Proxy port to use when sending requests
http.proxyUser
http.proxyPassword

If proxy is used, optional userid and password for it
http.noProxyFor
Hostnames or IPs matching this pattern will bypass the proxy

Integrating with GUI

When calling the Agent.logon() method, it will initially fail with errorcode AuthErrorCodes.ERROR_NOT_YET_COMPLETED to check if authentication succeeds, we should continually call login (with a delay of at least 2 seconds between calls) until it succeeds or fails with a different error.

Below is a brief example of parts of a Vaadin based application which initiates authentication, and repeatedly polls to obtain the status.

It starts by letting the user choose if he wants to authenticate using the same device he is on versus a(nother) mobile device - if it is the same device, we do not need to ask for his social security number, but we need it if he is using a different device.
For a different device, we prompt him for his social security number before initiating login.

/**
 * 
 * Login using BankID SE
 * 
 * All texts, flows and documentation is available at
 * 
 * https://www.bankid.com/assets/bankid/rp/bankid-relying-party-guidelines-v2.15.pdf
 *  
 * @author Kim Rasmussen
 * @version $Revision$
 *
 * <pre>
 * Ceptor - http://ceptor.io
 * 
 * This source code is confidential.
 * </pre>
 */
public class SwedishBankIDAuth extends VerticalLayout {
	private static final long serialVersionUID = 1L;
	private IController controller;
	private TextField personNummer;
	private Button next;
	private Button thisComputer;
	private Button mobileDevice;
	private Refresher refresher;
	private boolean isIdProvided = false;
	
	private boolean isMobileDevice = DeviceUtils.isMobileDevice();
	
	public SwedishBankIDAuth(IController controller) {
		this.controller = controller;
		
		setStyleName("small-padding");
		setWidth("100%");
		setMargin(false);
		
		populateInitialGUI();
	}

	private void populateInitialGUI() {
		removeAllComponents();
		// Differ messages depending on running in browser or on mobile
		String promptKey = isMobileDevice ? "RFA20" : "RFA19";
		addComponent(new Label(Language.getText(promptKey, promptKey)));
		
		thisComputer = new Button(Language.getText(promptKey+"_THIS", promptKey+"_THIS"), e -> useThisComputer());
		thisComputer.setStyleName("custom");
		
		mobileDevice = new Button(Language.getText(promptKey+"_MOBILE", promptKey+"_MOBILE"), e -> useMobileDevice());
		thisComputer.setStyleName("custom");
		
		HorizontalLayout hl = new HorizontalLayout();
		hl.addComponents(thisComputer, mobileDevice);
		addComponent(hl);
		
		// Start with clean slate
		try {
			Agent.getInstance().setStateVariable(JSPHelper.getSessionID(), "bankidse_orderRef", null, false);
			Agent.getInstance().setStateVariable(JSPHelper.getSessionID(), "bankidse_autoStartToken", null, false);
		} catch(PTException e) {
			// OK to ignore
		}
	}
	
	private void useThisComputer() {
		isIdProvided = false;
		
		// No ID needed, since we are using the same device
		try {
			// Credentials here ensure that only BankID mobile can be used.
			
			Agent.getInstance().logon(JSPHelper.getSessionID(), AuthTypes.AUTHTYPE_BANKID_SE, "", "{\"certificatePolicies\": [\"1.2.752.78.1.5\"]}"); // TODO: Need to consider test environment vs production
		} catch(PTException e) {
			if (e.getErrorCode() != AuthErrorCodes.ERROR_NOT_YET_COMPLETED) { // Error 19 means that authentication is started
				handleError(e);
				return;
			}
		}
		
		redirectNow();
	}

	private void redirectNow() {
		// If we get here, we can redirect
		try {
			String autoStartToken = Agent.getInstance().getStateVariable(JSPHelper.getSessionID(), "bankidse_autoStartToken");

			String challenge = Base64.encode(UUID.randomUUID().toString());
			Agent.getInstance().setStateVariable(JSPHelper.getSessionID(), "challenge", challenge, false);
			
			// Redirect to BankID on the same computer/device
			String url = "bankid:///?autostarttoken="+URLUtils.encodeURL(autoStartToken)+"&rpref="+URLUtils.encodeURL(challenge)+"&redirect="+URLUtils.encodeURL(Page.getCurrent().getLocation().toString());
			
			// When we get back, we need to poll immediately to see if it worked.
			initiatePoll();
			Page.getCurrent().setLocation(url);
		} catch (PTException e) {
			handleError(e);
		}
	}
	
	private void handleError(PTException e) {
		removeAllComponents();
		
		if (refresher != null) {
			removeExtension(refresher);
			refresher = null;
		}
		Label l = new Label(getMessage(e));
		l.addStyleName(ValoTheme.LABEL_FAILURE);
		addComponent(l);
		addComponent(new Button(Language.getText("bankid.retry", "Try again"), (source) -> populateInitialGUI()));
	}
	
	private void initiatePoll() {
		// Start the polling...
		refresher = new Refresher();
		refresher.setRefreshInterval(2000);
		addExtension(refresher);
		refresher.addListener(source -> pollStatus());
	}
	
	private void pollStatus() {
		try {
			// Credentials here ensure that only BankID mobile can be used.
			
			Agent.getInstance().logon(JSPHelper.getSessionID(), AuthTypes.AUTHTYPE_BANKID_SE, "", "");
			
			if (refresher != null) {
				removeExtension(refresher);
				refresher = null;
			}
			// It worked, login is done.
			controller.bankIDLoginCompleted();
			return;
		} catch(PTException e) {
			if (e.getErrorCode() != AuthErrorCodes.ERROR_NOT_YET_COMPLETED) { // Error 19 means that authentication is started
				handleError(e);
				return;
			}
			
			removeAllComponents();
			
			addComponent(new Label(getMessage(e)));
		}
		
		
	}
	
	private String getMessage(PTException e) {
		String progressStatus = e.getErrorText();
		if (progressStatus == null)
			return e.toString();
		String message = progressStatus;
		
		switch(progressStatus) {
		case "OUTSTANDING_TRANSACTION":
		case "NO_CLIENT":
			message = Language.getText("RFA1", "RFA1");
			break;
		case "ALREADY_IN_PROGRESS":
		case "CANCELLED":
			message = Language.getText("RFA3", "RFA3");
			break;
		case "RETRY":
		case "INTERNAL_ERROR":
			message = Language.getText("RFA5", "RFA5");
			break;
		case "USER_CANCEL":
			message = Language.getText("RFA6", "RFA6");
			break;
		case "EXPIRED_TRANSACTION":
			message = Language.getText("RFA8", "RFA8");
			break;
		case "USER_SIGN":
			message = Language.getText("RFA9", "RFA9");
			break;
		case "CLIENT_ERR":
			message = Language.getText("RFA12", "RFA12");
			break;
		case "STARTED":
			if (isMobileDevice) {
				if (isIdProvided)
					message = Language.getText("RFA14_B", "RFA14_B");
				else
					message = Language.getText("RFA15_B", "RFA15_B");
			} else { // PC is used
				if (isIdProvided)
					message = Language.getText("RFA14_A", "RFA14_A");
				else
					message = Language.getText("RFA15_A", "RFA15_A");
			}
			break;
		case "START_FAILED":
			message = Language.getText("RFA17", "RFA17");
			break;
		}
		
		return message;
	}
	/**
	 * Use mobile (or other device if already running on mobile)
	 */
	private void useMobileDevice() {
		// Prompt for ID
		setupPersonnummer();
	}
	
	private void setupPersonnummer() {
		removeAllComponents();
		
		personNummer = new TextField();
		personNummer.setPlaceholder("ID");
		personNummer.setIcon(VaadinIcons.USER);
		personNummer.setStyleName(ValoTheme.TEXTFIELD_INLINE_ICON);
		personNummer.setWidth("100%");
		addComponent(personNummer);
		
		next = new Button(Language.getText("RFA18", "RFA18"), e -> nextBtnClickListener());
		next.setStyleName("custom");
		addComponent(next);
		
		setComponentAlignment(next, Alignment.BOTTOM_RIGHT);
	}

	private void nextBtnClickListener() {
		if (personNummer.isEmpty()) {
			Notification.show(Language.getText("bankid.emptyid", "Please enter ID"));
			personNummer.focus();
			return;
		}
		
		isIdProvided = true;
		try {
			Agent.getInstance().logon(JSPHelper.getSessionID(), AuthTypes.AUTHTYPE_BANKID_SE, personNummer.getValue().trim(), "");
			controller.bankIDLoginCompleted();
		} catch(PTException e) {
			if (e.getErrorCode() != AuthErrorCodes.ERROR_NOT_YET_COMPLETED) { // Error 19 means that authentication is started
				handleError(e);
				return;
			}
		}
		
		removeAllComponents();
		addComponent(new Label(Language.getText("RFA1", "RFA1")));
		
		initiatePoll();
	}
}

The language texts refer to this list, which is taken from BankID's suggestions:

#BankID settings - values come from https://www.bankid.com/assets/bankid/rp/bankid-relying-party-guidelines-v2.15.pdf
bankidse.retry=Prøv igen
bankidse.emptyid=Indtast venligst personnummer

RFA1=Start your BankID app.
RFA2=The BankID app is not installed. Please contact your internet bank.
RFA3=Action cancelled. Please try again.
RFA5=Internal error. Please try again.
RFA6=Action cancelled.
RFA8=The BankID app is not responding. Please check that the program is started and that you have internet access. If you don\u2019t have a valid BankID you can get one from your bank. Try again.
RFA9=Enter your security code in the BankID app and select Identify or Sign.
RFA12=Internal error. Update your BankID app and try again.
RFA14_A=Searching for BankID:s, it may take a little while... If a few seconds have passed and still no BankID has been found, you probably don\u2019t have a BankID which can be used for this login/signature on this computer. If you have a BankID card, please insert it into your card reader. If you don\u2019t have a BankID you can order one from your internet bank. If you have a BankID on another device you can start the BankID app on that device.
RFA14_B=Searching for BankID:s, it may take a little while... If a few seconds have passed and still no BankID has been found, you probably don\u2019t have a BankID which can be used for this login/signature on this device. If you don\u2019t have a BankID you can order one from your internet bank. If you have a BankID on another device you can start the BankID app on that device.
RFA15_A=Searching for BankID:s, it may take a little while... If a few seconds have passed and still no BankID has been found, you probably don\u2019t have a BankID which can be used for this login/signature on this computer. If you have a BankID card, please insert it into your card reader. If you don\u2019t have a BankID you can order one from your internet bank.
RFA15_B=Searching for BankID:s, it may take a little while... If a few seconds have passed and still no BankID has been found, you probably don\u2019t have a BankID which can be used for this login/signature on this device. If you don\u2019t have a BankID you can order one from your internet bank
RFA16=The BankID you are trying to use is revoked or too old. Please use another BankID or order a new one from your internet bank.
RFA17=The BankID app couldn\u2019t be found on your computer or mobile device. Please install it and order a BankID from your internet bank. Install the app from install.bankid.com.
RFA18=Start the BankID app
RFA19=Would you like to login or sign with a BankID on this computer or with a Mobile BankID?
RFA20=Would you like to login or sign with a BankID on this device or with a BankID on another device?
RFA19_THIS=This computer
RFA20_THIS=This device
RFA19_MOBILE=Mobile BankID
RFA20_MOBILE=BankID on another device

State Variables Available in the Session After Authentication

Once authentication is completed, the following state variables are available within the session:

NameDescription
xmldsigXML Signature containing the signed data from BankID
bankid_ocspResponseBase64 encoded OCSP response as delivered from BankID
bankid_givenNameUsers given name
bankid_surnameUsers surname
bankid_nameComplete name
bankid_personalNumberUsers social security number
bankid_notBeforeTime of bankid authentication
bankid_notAfterTime that bankid authentication expires (note that this value is not actively used by Ceptor)
bankid_ipAddressIP address of users device used for BankID authentication.


When signing data, output of the sign operation are available in the same attributes, but prefixed with "sign_" instead of "bankid_".

Production Configuration

Refer to https://www.bankid.com/bankid-i-dina-tjanster/rp-info for details on URLs and certificates to use for production.

Testing

Ceptor comes with test certificates (see the example configuration above) downloaded from https://www.bankid.com 

To obtain devices to test with, you can e.g. configure the BankID iOS App with a test URL - see details here: https://www.bankid.com/bankid-i-dina-tjanster/rp-info

At bankid.com, you can also find information about how to create additional test users.

© Ceptor ApS. All Rights Reserved.