Skip to content

NFC-47 NFC support for web-eid example #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
26 changes: 25 additions & 1 deletion example/src/main/resources/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,11 @@ <h3><a id="for-developers"></a>For developers</h3>
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"
Expand All @@ -274,6 +278,22 @@ <h3><a id="for-developers"></a>For developers</h3>
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", {
Expand All @@ -299,6 +319,10 @@ <h3><a id="for-developers"></a>For developers</h3>
}
});

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'
Expand Down
44 changes: 44 additions & 0 deletions example/src/test/java/eu/webeid/example/WebApplicationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -99,6 +102,14 @@ public void validateOcspResponse(XadesSignature xadesSignature) {
}
};

new MockUp<SigningService>() {
@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)));

Expand Down Expand Up @@ -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<SubjectCertificateNotRevokedValidator>() {
@Mock
public void validateCertificateNotRevoked(X509Certificate subjectCertificate) {
// Do not call real OCSP service in tests.
}
};

new MockUp<AsicSignatureFinalizer>() {
@Mock
public void validateOcspResponse(XadesSignature xadesSignature) {
// Do not call real OCSP service in tests.
}
};

new MockUp<SigningService>() {
@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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -58,4 +60,23 @@ void testAttemptAuthentication() throws Exception {
new WebEidAjaxLoginProcessingFilter("/auth/login", authenticationManager)
.attemptAuthentication(request, response));
}
}

@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)
);
}
}
23 changes: 23 additions & 0 deletions example/src/test/java/eu/webeid/example/testutil/ObjectMother.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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() {
Expand All @@ -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);
}
Expand Down