Copy import java.io.BufferedReader;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import co.arculus.fido.ArculusFidoServer;
import co.arculus.fido.ArculusFidoServerResponse;
import co.arculus.fido.ArculusFidoException;
public class FidoServlet extends HttpServlet {
private static final String FIDO2_SERVER_URL = "https://fido.example.com";
private static final int HTTP_TIMEOUT = 30000; // 30 seconds
private ArculusFidoServer fidoServer;
private final Gson gson;
@Override
public void init() throws ServletException {
super.init();
this.gson = new Gson();
initializeFidoServer();
}
private void initializeFidoServer() {
try {
this.fidoServer = new ArculusFidoServer(FIDO2_SERVER_URL);
this.fidoServer.setBeginAuthenticatePath("/fidoapi/authenticate/begin", null);
this.fidoServer.setCompleteAuthenticatePath("/fidoapi/authenticate/complete", null);
this.fidoServer.setBeginRegistrationPath("/fidoapi/register/begin", null);
this.fidoServer.setCompleteRegistrationPath("/fidoapi/register/complete", null);
this.fidoServer.setHttpTimeout(HTTP_TIMEOUT);
} catch (Exception e) {
throw new ServletException("Failed to initialize FIDO2 server SDK", e);
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("application/json; charset=utf-8");
// Read request body
BufferedReader reader = request.getReader();
StringBuilder requestBody = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
requestBody.append(line);
}
JsonObject requestJson = JsonParser.parseString(requestBody.toString()).getAsJsonObject();
String operation = requestJson.has("operation") ?
requestJson.get("operation").getAsString() : null;
if (operation == null) {
sendErrorResponse(response, "Missing operation parameter", 400);
return;
}
switch (operation.toLowerCase()) {
case "register":
handleRegister(requestJson, response);
break;
case "authenticate":
handleAuthenticate(requestJson, response);
break;
default:
sendErrorResponse(response, "Unknown operation: " + operation, 400);
break;
}
}
private void handleRegister(JsonObject request, HttpServletResponse response)
throws IOException {
String username = request.get("username").getAsString();
String displayName = request.has("displayName") ?
request.get("displayName").getAsString() : username;
String rpId = request.has("rpId") ?
request.get("rpId").getAsString() : "example.com";
if (!request.has("cardResponseData")) {
// Phase 1: Registration Begin
handleRegisterBegin(username, displayName, rpId, response);
} else {
// Phase 3: Registration Complete
JsonObject cardResponseData = request.getAsJsonObject("cardResponseData");
String cookies = request.has("cookies") ?
request.get("cookies").getAsString() : null;
handleRegisterComplete(username, cardResponseData, cookies, response);
}
}
private void handleRegisterBegin(String username, String displayName, String rpId,
HttpServletResponse response) throws IOException {
try {
if (fidoServer == null) {
sendErrorResponse(response, "FIDO2 server SDK not initialized", 500);
return;
}
// Use Arculus SDK to call FIDO2 server
ArculusFidoServerResponse fidoResponse = fidoServer.callRegisterBegin(username, displayName, null);
JsonObject fidoJson = JsonParser.parseString(fidoResponse.getResponseBody()).getAsJsonObject();
if (fidoJson.has("error")) {
sendErrorResponse(response, "FIDO2 server error: " +
fidoJson.get("error").getAsString(), 500);
return;
}
// Extract FIDO2 server origin (protocol + host + port, no path)
String fido2ServerOrigin = extractFido2ServerOrigin();
// Build response for iOS app
JsonObject responseJson = new JsonObject();
responseJson.addProperty("status", "success");
responseJson.addProperty("operation", "register");
responseJson.addProperty("rpId", rpId);
responseJson.addProperty("username", username);
responseJson.add("fidoServerResponse", fidoJson);
if (fido2ServerOrigin != null) {
responseJson.addProperty("fido2ServerOrigin", fido2ServerOrigin);
}
// Pass cookies for session continuity
if (fidoResponse.getCookies() != null) {
responseJson.addProperty("cookies", fidoResponse.getCookies());
}
response.getWriter().write(gson.toJson(responseJson));
} catch (ArculusFidoException e) {
sendErrorResponse(response, "Registration begin failed: " +
(e.getMessage() != null ? e.getMessage() : "FIDO2 server communication failed"), 500);
} catch (Exception e) {
sendErrorResponse(response, "Registration begin failed: " +
(e.getMessage() != null ? e.getMessage() : "Unknown error"), 500);
}
}
private String extractFido2ServerOrigin() {
// Extract origin from FIDO2_SERVER_URL (protocol + host + port, no path)
try {
java.net.URL url = new java.net.URL(FIDO2_SERVER_URL);
int port = url.getPort();
if (port == -1) {
port = url.getDefaultPort();
}
String origin = url.getProtocol() + "://" + url.getHost();
if (port != -1 && port != url.getDefaultPort()) {
origin += ":" + port;
}
return origin;
} catch (Exception e) {
return FIDO2_SERVER_URL;
}
}
private void handleRegisterComplete(String username, JsonObject cardResponseData,
String cookies, HttpServletResponse response) throws IOException {
try {
if (fidoServer == null) {
sendErrorResponse(response, "FIDO2 server SDK not initialized", 500);
return;
}
// Clean cookies before sending to FIDO2 server
// HttpURLConnection (used by SDK) requires Cookie header to contain only name=value pairs
String cleanedCookies = cleanCookieString(cookies);
// Construct the full FIDO2 registration complete request
JsonObject fidoRequest = new JsonObject();
fidoRequest.addProperty("username", username);
// Add type, id, rawId from cardResponseData
if (cardResponseData.has("type")) {
fidoRequest.addProperty("type", cardResponseData.get("type").getAsString());
} else {
fidoRequest.addProperty("type", "public-key"); // Default
}
if (cardResponseData.has("id")) {
fidoRequest.addProperty("id", cardResponseData.get("id").getAsString());
}
if (cardResponseData.has("rawId")) {
fidoRequest.addProperty("rawId", cardResponseData.get("rawId").getAsString());
} else if (cardResponseData.has("id")) {
// Use id as rawId if rawId not present
fidoRequest.addProperty("rawId", cardResponseData.get("id").getAsString());
}
// Add response object (preserve exact structure from card)
if (cardResponseData.has("response")) {
fidoRequest.add("response", cardResponseData.getAsJsonObject("response"));
} else {
// Fallback: use entire cardResponseData as response
fidoRequest.add("response", cardResponseData);
}
String requestBody = gson.toJson(fidoRequest);
// Use Arculus SDK to call FIDO2 server
ArculusFidoServerResponse fidoResponse = fidoServer.callRegisterComplete(requestBody, cleanedCookies);
JsonObject fidoJson = JsonParser.parseString(fidoResponse.getResponseBody()).getAsJsonObject();
if (fidoJson.has("error")) {
sendErrorResponse(response, "FIDO2 registration failed: " +
fidoJson.get("error").getAsString(), 400);
return;
}
JsonObject responseJson = new JsonObject();
responseJson.addProperty("status", "success");
responseJson.addProperty("operation", "register");
responseJson.addProperty("message", "Registration completed successfully");
response.getWriter().write(gson.toJson(responseJson));
} catch (ArculusFidoException e) {
sendErrorResponse(response, "Registration complete failed: " +
(e.getMessage() != null ? e.getMessage() : "FIDO2 server communication failed"), 500);
} catch (Exception e) {
sendErrorResponse(response, "Registration complete failed: " +
(e.getMessage() != null ? e.getMessage() : "Unknown error"), 500);
}
}
/**
* Clean cookie string before sending to FIDO2 server.
* HttpURLConnection (used by SDK) requires Cookie header to contain only name=value pairs.
* This removes any "Cookie:" prefix and trims whitespace.
*/
private String cleanCookieString(String cookies) {
if (cookies == null || cookies.trim().isEmpty()) {
return null;
}
// Remove "Cookie:" prefix if present, and clean up whitespace
String cleaned = cookies.replaceFirst("(?i)^Cookie:\\s*", "").trim();
return cleaned.isEmpty() ? null : cleaned;
}
private void handleAuthenticate(JsonObject request, HttpServletResponse response)
throws IOException {
String username = request.get("username").getAsString();
String rpId = request.has("rpId") ?
request.get("rpId").getAsString() : "example.com";
if (!request.has("cardResponseData")) {
// Phase 1: Authentication Begin
handleAuthenticateBegin(username, rpId, response);
} else {
// Phase 3: Authentication Complete
JsonObject cardResponseData = request.getAsJsonObject("cardResponseData");
String cookies = request.has("cookies") ?
request.get("cookies").getAsString() : null;
handleAuthenticateComplete(username, cardResponseData, cookies, response);
}
}
private void handleAuthenticateBegin(String username, String rpId,
HttpServletResponse response) throws IOException {
try {
if (fidoServer == null) {
sendErrorResponse(response, "FIDO2 server SDK not initialized", 500);
return;
}
// Use Arculus SDK to call FIDO2 server
ArculusFidoServerResponse fidoResponse = fidoServer.callAuthenticateBegin(username, username, null);
JsonObject fidoJson = JsonParser.parseString(fidoResponse.getResponseBody()).getAsJsonObject();
if (fidoJson.has("error")) {
sendErrorResponse(response, "FIDO2 server error: " +
fidoJson.get("error").getAsString(), 500);
return;
}
String sessionId = null;
if (fidoJson.has("allowCredentials")) {
com.google.gson.JsonArray allowCredentials = fidoJson.getAsJsonArray("allowCredentials");
if (allowCredentials.size() > 0) {
JsonObject credential = allowCredentials.get(0).getAsJsonObject();
if (credential.has("id")) {
sessionId = credential.get("id").getAsString();
}
}
}
// Build response for iOS app
JsonObject responseJson = new JsonObject();
responseJson.addProperty("status", "success");
responseJson.addProperty("operation", "authenticate");
responseJson.addProperty("rpId", rpId);
responseJson.addProperty("username", username);
responseJson.add("fidoServerResponse", fidoJson);
if (sessionId != null) {
responseJson.addProperty("sessionId", sessionId);
}
// CRITICAL: Pass FIDO2 server URL to mobile app for clientDataJSON origin
String fido2ServerOrigin = extractFido2ServerOrigin();
responseJson.addProperty("fido2ServerOrigin", fido2ServerOrigin);
// Pass cookies for session continuity
if (fidoResponse.getCookies() != null) {
responseJson.addProperty("cookies", fidoResponse.getCookies());
}
response.getWriter().write(gson.toJson(responseJson));
} catch (ArculusFidoException e) {
sendErrorResponse(response, "Authentication begin failed: " +
(e.getMessage() != null ? e.getMessage() : "FIDO2 server communication failed"), 500);
} catch (Exception e) {
sendErrorResponse(response, "Authentication begin failed: " +
(e.getMessage() != null ? e.getMessage() : "Unknown error"), 500);
}
}
private void handleAuthenticateComplete(String username, JsonObject cardResponseData,
String cookies, HttpServletResponse response) throws IOException {
try {
if (fidoServer == null) {
sendErrorResponse(response, "FIDO2 server SDK not initialized", 500);
return;
}
// Clean cookies before sending to FIDO2 server
String cleanedCookies = cleanCookieString(cookies);
String type = cardResponseData.has("type") ?
cardResponseData.get("type").getAsString() : "public-key";
String id = cardResponseData.has("id") ?
cardResponseData.get("id").getAsString() : null;
// Preserve the exact responseData structure as received from the card
JsonObject responseData = cardResponseData.has("response") ?
cardResponseData.getAsJsonObject("response") : new JsonObject();
// Prepare authentication completion request to FIDO2 server
JsonObject fidoRequest = new JsonObject();
fidoRequest.addProperty("username", username);
fidoRequest.addProperty("type", type);
if (id != null) {
fidoRequest.addProperty("id", id);
}
fidoRequest.add("response", responseData);
String requestBody = gson.toJson(fidoRequest);
// Use Arculus SDK to call FIDO2 server
co.arculus.fido.internal.FidoEndpoint endpoint = fidoServer.getCompleteAuthenticateEndpoint();
ArculusFidoServerResponse fidoResponse;
try {
fidoResponse = fidoServer.makeHttpCall(endpoint, requestBody, cleanedCookies);
} catch (ArculusFidoException e) {
int serverResponseCode = e.getUnderlyingServerResponseCode();
if (serverResponseCode == 401) {
sendErrorResponse(response,
"FIDO2 server session expired. Please retry authentication from the beginning.", 401);
return;
}
sendErrorResponse(response, "FIDO2 server communication failed (HTTP " + serverResponseCode + "): " +
(e.getMessage() != null ? e.getMessage() : "Unknown error"), 500);
return;
}
JsonObject fidoJson = JsonParser.parseString(fidoResponse.getResponseBody()).getAsJsonObject();
if (fidoJson.has("error")) {
sendErrorResponse(response, "FIDO2 authentication failed: " +
fidoJson.get("error").getAsString(), 400);
return;
}
JsonObject responseJson = new JsonObject();
responseJson.addProperty("status", "success");
responseJson.addProperty("operation", "authenticate");
responseJson.addProperty("message", "Authentication completed successfully");
response.getWriter().write(gson.toJson(responseJson));
} catch (ArculusFidoException e) {
sendErrorResponse(response, "Authentication complete failed: " +
(e.getMessage() != null ? e.getMessage() : "FIDO2 server communication failed"), 500);
} catch (Exception e) {
sendErrorResponse(response, "Authentication complete failed: " +
(e.getMessage() != null ? e.getMessage() : "Unknown error"), 500);
}
}
private void sendErrorResponse(HttpServletResponse response, String message, int statusCode)
throws IOException {
response.setStatus(statusCode);
JsonObject errorJson = new JsonObject();
errorJson.addProperty("status", "failed");
errorJson.addProperty("error", message);
response.getWriter().write(errorJson.toString());
}
}