diff --git a/example/src/main/java/eu/webeid/example/web/rest/ChallengeController.java b/example/src/main/java/eu/webeid/example/web/rest/ChallengeController.java
index df54366a..85dc2122 100644
--- a/example/src/main/java/eu/webeid/example/web/rest/ChallengeController.java
+++ b/example/src/main/java/eu/webeid/example/web/rest/ChallengeController.java
@@ -40,7 +40,16 @@ public ChallengeController(ChallengeNonceGenerator challengeNonceGenerator) {
@GetMapping("challenge")
public ChallengeDTO challenge() {
- final ChallengeDTO challenge = new ChallengeDTO();
+ return generateChallenge();
+ }
+
+ @GetMapping("mobile/challenge")
+ public ChallengeDTO mobileChallenge() {
+ return generateChallenge();
+ }
+
+ private ChallengeDTO generateChallenge() {
+ ChallengeDTO challenge = new ChallengeDTO();
challenge.setNonce(challengeNonceGenerator.generateAndStoreNonce().getBase64EncodedNonce());
return challenge;
}
diff --git a/example/src/main/resources/templates/index.html b/example/src/main/resources/templates/index.html
index 051ab855..edd8a21c 100644
--- a/example/src/main/resources/templates/index.html
+++ b/example/src/main/resources/templates/index.html
@@ -265,7 +265,11 @@
For developers
authButton.disabled = true;
try {
- const challengeResponse = await fetch("/auth/challenge", {
+ const challengeUrl = isMobileDevice()
+ ? "/auth/mobile/challenge"
+ : "/auth/challenge";
+
+ const challengeResponse = await fetch(challengeUrl, {
method: "GET",
headers: {
"Content-Type": "application/json"
@@ -274,6 +278,22 @@ For developers
await checkHttpError(challengeResponse);
const {nonce} = await challengeResponse.json();
+ if (isMobileDevice()) {
+ const loginUri = encodeURIComponent(window.location.origin + "/");
+ const payload = {
+ challenge: nonce,
+ login_uri: loginUri
+ };
+ const encoded = btoa(JSON.stringify(payload));
+
+ // TODO: Replace with actual URL once DigiDoc app supports app links
+ const eidAppUri = `TODO-MOBILE-EID-APP-LAUNCH-URL#${encoded}`;
+
+ alert("Opening eID app for authentication...\n\nIf nothing happens, the app may not yet support mobile login.");
+ window.location.href = eidAppUri;
+ return;
+ }
+
const authToken = await webeid.authenticate(nonce, {lang});
const authTokenResponse = await fetch("/auth/login", {
@@ -299,6 +319,10 @@ For developers
}
});
+ function isMobileDevice() {
+ return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
+ }
+
document.addEventListener('DOMContentLoaded', function () {
setTimeout(function () {
document.querySelector(".eu-logo-fixed").style.display = 'none'
diff --git a/example/src/test/java/eu/webeid/example/WebApplicationTest.java b/example/src/test/java/eu/webeid/example/WebApplicationTest.java
index f7f5a3de..38cfbf1d 100644
--- a/example/src/test/java/eu/webeid/example/WebApplicationTest.java
+++ b/example/src/test/java/eu/webeid/example/WebApplicationTest.java
@@ -22,6 +22,9 @@
package eu.webeid.example;
+import eu.webeid.example.service.SigningService;
+import eu.webeid.example.service.dto.FileDTO;
+import eu.webeid.example.service.dto.SignatureDTO;
import eu.webeid.example.testutil.Dates;
import eu.webeid.example.testutil.HttpHelper;
import eu.webeid.example.testutil.ObjectMother;
@@ -99,6 +102,14 @@ public void validateOcspResponse(XadesSignature xadesSignature) {
}
};
+ new MockUp() {
+ @Mock
+ public FileDTO signContainer(SignatureDTO signatureDTO) {
+ // Return a dummy FileDTO in tests
+ return new FileDTO("example-for-signing.asice");
+ }
+ };
+
MockHttpSession session = new MockHttpSession();
session.setAttribute("challenge-nonce", new ChallengeNonce(ObjectMother.VALID_CHALLENGE_NONCE, DateAndTime.utcNow().plusMinutes(1)));
@@ -132,4 +143,37 @@ public static MockMultipartFile mockMultipartFile() {
assertEquals(HttpStatus.OK.value(), response.getStatus());
assertEquals("attachment; filename=example-for-signing.asice", response.getHeader("Content-Disposition"));
}
+
+ @Test
+ public void testHappyFlow_NfcLoginAuthToken() throws Exception {
+ new MockUp() {
+ @Mock
+ public void validateCertificateNotRevoked(X509Certificate subjectCertificate) {
+ // Do not call real OCSP service in tests.
+ }
+ };
+
+ new MockUp() {
+ @Mock
+ public void validateOcspResponse(XadesSignature xadesSignature) {
+ // Do not call real OCSP service in tests.
+ }
+ };
+
+ new MockUp() {
+ @Mock
+ public FileDTO signContainer(SignatureDTO signatureDTO) {
+ // Return a dummy FileDTO in tests
+ return new FileDTO("example-for-signing.asice");
+ }
+ };
+
+ MockHttpSession session = new MockHttpSession();
+ session.setAttribute("challenge-nonce", new ChallengeNonce(ObjectMother.VALID_CHALLENGE_NONCE, DateAndTime.utcNow().plusMinutes(1)));
+
+ MvcResult result = HttpHelper.login(mvcBuilder, session, ObjectMother.mockNfcAuthToken());
+ MockHttpServletResponse response = result.getResponse();
+ assertEquals("{\"sub\":\"JAAK-KRISTJAN JÕEORG\",\"auth\":\"[ROLE_USER]\"}", response.getContentAsString());
+ }
+
}
diff --git a/example/src/test/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilterTest.java b/example/src/test/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilterTest.java
index 828399b1..e0c08dcf 100644
--- a/example/src/test/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilterTest.java
+++ b/example/src/test/java/eu/webeid/example/security/WebEidAjaxLoginProcessingFilterTest.java
@@ -22,6 +22,7 @@
package eu.webeid.example.security;
+import eu.webeid.example.testutil.ObjectMother;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Test;
@@ -30,6 +31,7 @@
import java.io.BufferedReader;
import java.io.StringReader;
+import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.Mockito.mock;
@@ -58,4 +60,23 @@ void testAttemptAuthentication() throws Exception {
new WebEidAjaxLoginProcessingFilter("/auth/login", authenticationManager)
.attemptAuthentication(request, response));
}
-}
\ No newline at end of file
+
+ @Test
+ void testAttemptAuthentication_NfcToken() throws Exception {
+ final HttpServletRequest request = mock(HttpServletRequest.class);
+ final HttpServletResponse response = mock(HttpServletResponse.class);
+ when(request.getMethod()).thenReturn(HttpMethod.POST.name());
+ when(request.getHeader("Content-type")).thenReturn("application/json");
+
+ var jsonBody = ObjectMother.toJson(Map.of("auth-token", ObjectMother.mockNfcAuthToken().getToken()));
+
+ when(request.getReader()).thenReturn(new BufferedReader(new StringReader(jsonBody)));
+
+ final AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
+
+ assertDoesNotThrow(() ->
+ new WebEidAjaxLoginProcessingFilter("/auth/login", authenticationManager)
+ .attemptAuthentication(request, response)
+ );
+ }
+}
diff --git a/example/src/test/java/eu/webeid/example/testutil/ObjectMother.java b/example/src/test/java/eu/webeid/example/testutil/ObjectMother.java
index 288b1368..e92e70f0 100644
--- a/example/src/test/java/eu/webeid/example/testutil/ObjectMother.java
+++ b/example/src/test/java/eu/webeid/example/testutil/ObjectMother.java
@@ -48,6 +48,7 @@ public class ObjectMother {
private static final String TEST_PKI_CONTAINER = "src/test/resources/signout.p12";
private static final String TEST_PKI_CONTAINER_PASSWORD = "test";
private static final WebEidAuthToken VALID_AUTH_TOKEN;
+ private static final WebEidAuthToken VALID_NFC_AUTH_TOKEN;
static {
try {
@@ -63,6 +64,22 @@ public class ObjectMother {
}
}
+ static {
+ try {
+ VALID_NFC_AUTH_TOKEN = MAPPER.readValue(
+ "{\"algorithm\":\"ES384\"," +
+ "\"unverifiedCertificate\":\"MIIEBDCCA2WgAwIBAgIQY5OGshxoPMFg+Wfc0gFEaTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIxMDcyMjEyNDMwOFoXDTI2MDcwOTIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQmwEKsJTjaMHSaZj19hb9EJaJlwbKc5VFzmlGMFSJVk4dDy+eUxa5KOA7tWXqzcmhh5SYdv+MxcaQKlKWLMa36pfgv20FpEDb03GCtLqjLTRZ7649PugAQ5EmAqIic29CjggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPlp/ceABC52itoqppEmbf71TJz6MGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDCAgybz0u3W+tGI+AX+PiI5CrE9ptEHO5eezR1Jo4j7iGaO0i39xTGUB+NSC7P6AQbyE/ywqJjA1a62jTLcS9GHAJCARxN4NO4eVdWU3zVohCXm8WN3DWA7XUcn9TZiLGQ29P4xfQZOXJi/z4PNRRsR4plvSNB3dfyBvZn31HhC7my8woi\"," +
+ "\"unverifiedSigningCertificate\":\"MIIEBDCCA2WgAwIBAgIQY5OGshxoPMFg+Wfc0gFEaTAKBggqhkjOPQQDBDBgMQswCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MB4XDTIxMDcyMjEyNDMwOFoXDTI2MDcwOTIxNTk1OVowfzELMAkGA1UEBhMCRUUxKjAoBgNVBAMMIUrDlUVPUkcsSkFBSy1LUklTVEpBTiwzODAwMTA4NTcxODEQMA4GA1UEBAwHSsOVRU9SRzEWMBQGA1UEKgwNSkFBSy1LUklTVEpBTjEaMBgGA1UEBRMRUE5PRUUtMzgwMDEwODU3MTgwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQmwEKsJTjaMHSaZj19hb9EJaJlwbKc5VFzmlGMFSJVk4dDy+eUxa5KOA7tWXqzcmhh5SYdv+MxcaQKlKWLMa36pfgv20FpEDb03GCtLqjLTRZ7649PugAQ5EmAqIic29CjggHDMIIBvzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIDiDBHBgNVHSAEQDA+MDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAIBgYEAI96AQIwHwYDVR0RBBgwFoEUMzgwMDEwODU3MThAZWVzdGkuZWUwHQYDVR0OBBYEFPlp/ceABC52itoqppEmbf71TJz6MGEGCCsGAQUFBwEDBFUwUzBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAfBgNVHSMEGDAWgBTAhJkpxE6fOwI09pnhClYACCk+ezBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9haWEuZGVtby5zay5lZS9lc3RlaWQyMDE4MDUGCCsGAQUFBzAChilodHRwOi8vYy5zay5lZS9UZXN0X29mX0VTVEVJRDIwMTguZGVyLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDCAgybz0u3W+tGI+AX+PiI5CrE9ptEHO5eezR1Jo4j7iGaO0i39xTGUB+NSC7P6AQbyE/ywqJjA1a62jTLcS9GHAJCARxN4NO4eVdWU3zVohCXm8WN3DWA7XUcn9TZiLGQ29P4xfQZOXJi/z4PNRRsR4plvSNB3dfyBvZn31HhC7my8woi\"," +
+ "\"supportedSignatureAlgorithms\":[{\"cryptoAlgorithm\":\"RSA\",\"hashFunction\":\"SHA-256\",\"paddingScheme\":\"PKCS1.5\"}]," +
+ "\"issuerApp\":\"https://web-eid.eu/web-eid-mobile-app/releases/v1.0.0\"," +
+ "\"signature\":\"0Ov7ME6pTY1K2GXMj8Wxov/o2fGIMEds8OMY5dKdkB0nrqQX7fG1E5mnsbvyHpMDecMUH6Yg+p1HXdgB/lLqOcFZjt/OVXPjAAApC5d1YgRYATDcxsR1zqQwiNcHdmWn\"," +
+ "\"format\":\"web-eid:1.1\"}",
+ WebEidAuthToken.class);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("NFC token parsing failed");
+ }
+ }
+
public static final String VALID_CHALLENGE_NONCE = "12345678123456781234567812345678912356789123";
public static AuthTokenDTO mockAuthToken() {
@@ -71,6 +88,12 @@ public static AuthTokenDTO mockAuthToken() {
return authToken;
}
+ public static AuthTokenDTO mockNfcAuthToken() {
+ AuthTokenDTO authToken = new AuthTokenDTO();
+ authToken.setToken(VALID_NFC_AUTH_TOKEN);
+ return authToken;
+ }
+
public static String toJson(Object object) throws JsonProcessingException {
return MAPPER.writeValueAsString(object);
}