Mobile-ID Java client is a Java library that can be used for easy integration with MID REST interface (https://github.com/SK-EID/MID) of the Mobile-ID.
- A simple interface for user authentication
- A simple interface for digital signature services
- Java 1.8 (or newer)
- Access to Mobile-ID demo environment (to run integration tests)
You can use the library as a dependency from the Maven Central (http://mvnrepository.com/artifact/ee.sk.mid/mid-rest-java-client)
<dependency>
<groupId>ee.sk.mid</groupId>
<artifactId>mid-rest-java-client</artifactId>
<version>INSERT_VERSION_HERE</version>
</dependency>
compile group: 'ee.sk.mid', name: 'mid-rest-java-client', version: 'INSERT_VERSION_HERE'
- Running Unit tests
- Running against Demo environment
- How to forward requests to your phone
- Client configuration
- Retrieving signing certificate
- Creating the signature
- Authentication
- Handling negative scenarios
- Validating user input
mvn test
SK ID Solutions AS hosts a public demo environment that you can use for testing your integration. There are test numbers that can be used to simulate different scenarios.
The integration tests in this library have been configured to run against this Demo environment.
You can run only integration tests with:
mvn failsafe:integration-test
As a quick start you can also run MobileIdAuthenticationInteractive.class
from command line
or with your IDE by running its main method.
Forwarding requests to a real phone is no longer possible with demo environment.
InputStream is = TestData.class.getResourceAsStream("/path/to/truststore.jks");
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(is, "changeit".toCharArray());
MidClient client = MidClient.newBuilder()
.withHostUrl("https://tsp.demo.sk.ee/mid-api")
.withRelyingPartyUUID("00000000-0000-0000-0000-000000000000")
.withRelyingPartyName("DEMO")
.withTrustStore(trustStore)
.build();
Note that these values are demo environment specific. In production use the values provided by Application Provider.
The Relying Party needs to verify that it is connecting to MID API it trusts. More info about this requirement can be found from MID Documentation.
Server SSL certificates are valid for limited time and thus replaced regularly (about once in every 3 years). Every time a new certificate is issued, Relying Parties are notified in advance by Application Provider, and the new certificate needs to be imported into the Service Provider's system, or the code starts to throw errors after server certificate becomes invalid.
Following options are available to set trusted certificates.
Trust Store file is passed to mid-rest-java-client (recommended):
InputStream is = TestData.class.getResourceAsStream("/path/to/truststore.jks");
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(is, "changeit".toCharArray());
client = MidClient.newBuilder()
.withRelyingPartyUUID("00000000-0000-0000-0000-000000000000")
.withRelyingPartyName("DEMO")
.withHostUrl("https://tsp.demo.sk.ee/mid-api")
.withTrustStore(trustStore)
.build();
Note You can also use trust store in P12 format. In this case replace "JKS" with "PKCS12".
Using Trust Store is preferred as you can use the same format to keep track which certificates you trust.
Read chapter Validate returned certificate is a trusted MID certificate for more info.
...
InputStream is = TestData.class.getResourceAsStream("/path/to/truststore.jks");
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(is, "changeit".toCharArray());
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
trustManagerFactory.init(trustStore);
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
client = MidClient.newBuilder()
.withRelyingPartyUUID("00000000-0000-0000-0000-000000000000")
.withRelyingPartyName("DEMO")
.withHostUrl("https://tsp.demo.sk.ee/mid-api")
.withTrustSslContext(sslContext)
.build();
client = MidClient.newBuilder()
.withRelyingPartyUUID("00000000-0000-0000-0000-000000000000")
.withRelyingPartyName("DEMO")
.withHostUrl("https://tsp.demo.sk.ee/mid-api")
.withTrustedCertificates("PEM encoded cert 1", "PEM encoded cert 2")
.build();
Production server (mid.sk.ee) certificate will be available here: https://www.skidsolutions.eu/resources/certificates/#Other-certificates
Demo server (tsp.demo.sk.ee) certificate will be available here: https://www.skidsolutions.eu/resources/certificates/#Test-certificates
Import obtained server certificate into Java keystore:
keytool -import -file truststoreCert.pem -alias alias -keystore truststore.jks
If you want you can then convert the Java keystore to a P12 key store and use it instead
keytool -importkeystore -srckeystore production_server_trusted_ssl_certs.jks -destkeystore production_server_trusted_ssl_certs.p12 -srcstoretype JKS -deststoretype PKCS12
Integration tests (*IntegrationTest.java) that check the validity of server are configured not to run after server's certificate expiration. When server (either production server or demo server) certificate has expired then to make the tests run again one needs to replace certificate value in respective constant and import it into the trust store. Here is the process that needs to be followed.
-
Obtain the new certificate (see a few chapters above for where to download)
-
Replace the certificate value in LIVE_SERVER_CERT or in DEMO_SERVER_CERT constant.
-
Update the certificate expiration date in LIVE_SERVER_CERT_EXPIRATION_DATE or in DEMO_SERVER_CERT_EXPIRATION_DATE.
-
Import the new production (mid.sk.ee) certificate into production_server_trusted_ssl_certs.jks or the new demo (tsp.demo.sk.ee) certificate into demo_server_trusted_ssl_certs.jks like this:
Change into directory src/test/resources
DEMO:
keytool -importcert -file new.tsp.demo.sk.ee.certificate.cer -keystore demo_server_trusted_ssl_certs.jks -alias "tsp.demo.sk.ee that expires YYYY-MM-DD"
password: changeit
trust this certificate: yes
LIVE:
keytool -importcert -file new.mid.sk.ee.certificate.cer -keystore production_server_trusted_ssl_certs.jks -alias "mid.sk.ee that expires 2023.03.18"
password: changeit
trust this certificate: yes
- If it was production server's certificate that expired you also need to convert JKS keyestore to P12 keystore:
cd src/test/resources
keytool -importkeystore -srckeystore production_server_trusted_ssl_certs.jks -destkeystore production_server_trusted_ssl_certs.p12 -srcstoretype JKS -deststoretype PKCS12
Enter destination keystore password: changeit Enter source keystore password: changeit Existing entry alias trusted_mid_server_certs exists, overwrite?: yes Existing entry alias mid.sk.ee that expires YYYY-MM_DD exists, overwrite?: yes
- You need to add the new expiration dates of the imported certificates into the constants
of class ee.sk.mid.integration.MobileIdSsIT
LIVE_SERVER_CERT_EXPIRATION_DATE = LocalDate.of(/* add new date of expiry */);
DEMO_SERVER_CERT_EXPIRATION_DATE = LocalDate.of(/* add new date of expiry */);
After following this process the tests (that were ignored programmatically) should run again (check that there are no ignored tests) and a Pull Request could be submitted.
If you need to access the internet through a proxy (that runs on 127.0.0.1:3128 in the examples) you have two alternatives:
org.jboss.resteasy.client.jaxrs.ResteasyClient resteasyClient =
new org.jboss.resteasy.client.jaxrs.internal.ResteasyClientBuilderImpl()
.defaultProxy("127.0.0.1", 3128, "http")
.build();
MidClient client = MidClient.newBuilder()
.withHostUrl("https://tsp.demo.sk.ee/mid-api")
.withRelyingPartyUUID("00000000-0000-0000-0000-000000000000")
.withRelyingPartyName("DEMO")
.withConfiguredClient(resteasyClient)
.withTrustStore(trustStore)
.build();
org.glassfish.jersey.client.ClientConfig clientConfig =
new org.glassfish.jersey.client.ClientConfig();
clientConfig.property(ClientProperties.PROXY_URI, "127.0.0.1:3128");
MidClient client = MidClient.newBuilder()
.withHostUrl("https://tsp.demo.sk.ee/mid-api")
.withRelyingPartyUUID("00000000-0000-0000-0000-000000000000")
.withRelyingPartyName("DEMO")
.withTrustStore(trustStore)
.withNetworkConnectionConfig(clientConfig)
.build();
Under the hood operations as signing and authentication consist of 2 request steps:
- Initiation request
- Session status request
Session status request by default is a long poll method, meaning it might not return until a timeout expires.
The caller can tune the request parameters inside the bounds set by a service operator by using the withLongPollingTimeoutSeconds(int)
:
MidClient client = MidClient.newBuilder()
// set hostUrl, relyingPartyUUID, relyingPartyName and trustStore/trustSslContext
.withLongPollingTimeoutSeconds(60)
.build();
Check Long polling documentation chapter for more information.
If for some reason you cannot use long polling (which is recommended)
then you need to set withPollingSleepTimeoutSeconds(int)
to a few seconds (between 1...5).
This makes a request to Application Provider, the response is returned immediately
and if the session is not completed the client performs a sleep for configured amount of seconds
before making a new request.
MidClient client = MidClient.newBuilder()
// set hostUrl, relyingPartyUUID, relyingPartyName and trustStore/trustSslContext
.withPollingSleepTimeoutSeconds(2)
.build();
If you don't set a positive value either to longPollingTimeoutSeconds or pollingSleepTimeoutSeconds then pollingSleepTimeoutSeconds defaults to value 3 seconds.
In order to create signed container one needs to know the certificate of the user which can be obtained with a separate request:
MidCertificateRequest request = MidCertificateRequest.newBuilder()
.withPhoneNumber("+37200000266")
.withNationalIdentityNumber("60001019939")
.build();
MidCertificateChoiceResponse response = client.getMobileIdConnector().getCertificate(request);
X509Certificate certificate = client.createMobileIdCertificate(response);
There are convenience methods to read and validate phone number and national identity number entered by the user. See chapter Validating user input.
You can pass raw data to builder of SignableHash and it creates the hash itself internally:
byte[] data = "MY_DATA".getBytes(StandardCharsets.UTF_8);
MidHashToSign hashToSign = MidHashToSign.newBuilder()
.withDataToHash(data)
.withHashType( MidHashType.SHA256)
.build();
String verificationCode = hashToSign.calculateVerificationCode();
MidSignatureRequest request = MidSignatureRequest.newBuilder()
.withPhoneNumber("+37200000766")
.withNationalIdentityNumber("60001019906")
.withHashToSign(hashToSign)
.withLanguage( MidLanguage.ENG)
.withDisplayText("Sign document?")
.withDisplayTextFormat( MidDisplayTextFormat.GSM7)
.build();
MidSignatureResponse response = client.getMobileIdConnector().sign(request);
MidSessionStatus sessionStatus = client.getSessionStatusPoller().fetchFinalSessionStatus(response.getSessionID(),
"/signature/session/{sessionId}");
MidSignature signature = client.createMobileIdSignature(sessionStatus);
Note that
verificationCode
of the service should be displayed on the screen, so the person could verify if the verification code displayed on the screen and code sent him as a text message are identical.
Java demo application mid-rest-java-demo demonstrates how to create and sign a container with Mobile-ID and digidoc4j library.
MidHashToSign hashToSign = MidHashToSign.newBuilder()
.withHashInBase64("AE7S1QxYjqtVv+Tgukv2bMMi9gDCbc9ca2vy/iIG6ug=")
.withHashType( MidHashType.SHA256)
.build();
For security reasons, a new hash value must be created for each new authentication request.
MidAuthenticationHashToSign authenticationHash = MidAuthenticationHashToSign.generateRandomHashOfDefaultType();
String verificationCode = authenticationHash.calculateVerificationCode();
MidAuthenticationRequest request = MidAuthenticationRequest.newBuilder()
.withPhoneNumber("+37200000766")
.withNationalIdentityNumber("60001019906")
.withHashToSign(authenticationHash)
.withLanguage( MidLanguage.ENG)
.withDisplayText("Log into self-service?")
.withDisplayTextFormat( MidDisplayTextFormat.GSM7)
.build();
MidAuthenticationResponse response = client.getMobileIdConnector().authenticate(request);
MidSessionStatus sessionStatus = client.getSessionStatusPoller().fetchFinalSessionStatus(response.getSessionID(),
"/authentication/session/{sessionId}");
MidAuthentication authentication = client.createMobileIdAuthentication(sessionStatus, authenticationHash);
Note that
verificationCode
of the service should be displayed on the screen, so the person could verify if the verification code displayed on the screen and code sent him as a text message are identical.
Java demo application mid-rest-java-demo and PHP demo application mid-rest-php-demo demonstrate how to perform authentication and verify the response.
InputStream is = TestData.class.getResourceAsStream("/path/to/truststore.jks");
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(is, "changeit".toCharArray());
MidAuthenticationResponseValidator validator = new MidAuthenticationResponseValidator(trustStore);
MidAuthenticationResult authenticationResult = validator.validate(authentication);
assertThat(authenticationResult.isValid(), is(true));
assertThat(authenticationResult.getErrors().isEmpty(), is(true));
When the authentication result is valid a session could be created now within the e-service or application.
To avoid man-in-the-middle attacks you need to make sure the authentication certificate returned by MID API is issued by Application Provider (SK ID Solutions AS). You can read more about this requirement from MID API documentation.
You need to keep a Trust Store that trusts certificates taken from SK Certificate Repository. You need the following certificates:
For testing you need to import certificates for testing. You need the following certificates:
You can use the same Trust Store file that you keep trusted SSL server certificates (see chapter Verifying the SSL connection to Application Provider (SK)).
When the authentication result is not valid or the returned certificate is not signed by a CA that we trust then the reasons for invalidity are obtainable like this:
List<String> errors = authenticationResult.getErrors();
AuthenticationIdentity
could be helpful for obtaining information about the authenticated person when constructing the session.
MidAuthenticationIdentity authenticationIdentity = authenticationResult.getAuthenticationIdentity();
String givenName = authenticationIdentity.getGivenName();
String surName = authenticationIdentity.getSurName();
String identityCode = authenticationIdentity.getIdentityCode();
String country = authenticationIdentity.getCountry();
If user cancels the operation or the phone is unreachable then specific exceptions are thrown. These can be caught and handled locally.
Following exceptions indicate problems with integration or configuration on Relying Party (integrator) side:
MidSessionNotFoundException
, MissingOrInvalidParameterException
, UnauthorizedException
.
MidInternalErrorException
is for MID internal errors that cannot be handled by clients.
try {
// perform authentication or signing
}
catch (MidUserCancellationException e) {
logger.info("User cancelled operation from his/her phone.");
// display error
}
catch (MidNotMidClientException e) {
logger.info("User is not a MID client or user's certificates are revoked.");
// display error
}
catch (MidSessionTimeoutException e) {
logger.info("User did not type in PIN code or communication error.");
// display error
}
catch (MidPhoneNotAvailableException e) {
logger.info("Unable to reach phone/SIM card. User needs to check if phone has coverage.");
// display error
}
catch (MidDeliveryException e) {
logger.info("Error communicating with the phone/SIM card.");
// display error
}
catch (MidInvalidUserConfigurationException e) {
logger.info("Mobile-ID configuration on user's SIM card differs from what is configured on service provider's side. User needs to contact his/her mobile operator.");
logger.info("In case of DEMO the user needs to re-import MID certificate at https://demo.sk.ee/MIDCertsReg/");
// display error
}
catch (MidSessionNotFoundException | MidMissingOrInvalidParameterException | MidUnauthorizedException e) {
logger.error("Integrator-side error with MID integration or configuration", e);
// navigate to error page
}
catch (MidServiceUnavailableException e) {
logger.warn("MID service is currently unavailable. Please try again later.");
// navigate to error page
}
catch (MidInternalErrorException e) {
logger.warn("MID service returned internal error that cannot be handled locally.");
// navigate to error page
}
If you request signing certificate in a separate try block then you need to handle following exceptions separately:
try {
// request user signing certificates
}
catch (MidNotMidClientException e) {
logger.info("User is not a MID client or user's certificates are revoked");
}
catch (MidMissingOrInvalidParameterException | MidUnauthorizedException e) {
logger.error("Integrator-side error with MID integration (including insufficient input validation) or configuration", e);
}
catch (MidInternalErrorException e) {
logger.warn("MID service returned internal error that cannot be handled locally.");
}
This library comes with convenience methods to validate user input. You can use the methods also to clean input from whitespaces.
try {
String nationalIdentityNumber = MidInputUtil.getValidatedNationalIdentityNumber("<national identity number entered by user>");
String phoneNumber = MidInputUtil.getValidatedPhoneNumber("<phone number entered by user>");
}
catch (MidInvalidNationalIdentityNumberException e) {
logger.info("User entered invalid national identity number");
// display error
}
catch (MidInvalidPhoneNumberException e) {
logger.info("User entered invalid phone number");
// display error
}
This library uses Logback for logging. To log incoming and outgoing requests made by the library set following class to log at 'trace' level:
<logger name="ee.sk.mid.rest.MidLoggingFilter" level="trace" additivity="false">
<appender-ref ref="Console" />
</logger>
This project is licensed under the terms of the MIT license.