package at.gv.egovernment.moa.id.auth.modules.sl20_auth.tasks; import java.io.IOException; import java.io.StringWriter; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.http.entity.ContentType; import org.jose4j.base64url.Base64Url; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import at.gv.egiz.eaaf.core.api.data.EAAFConstants; import at.gv.egiz.eaaf.core.api.idp.process.ExecutionContext; import at.gv.egiz.eaaf.core.exceptions.TaskExecutionException; import at.gv.egiz.eaaf.core.impl.idp.auth.modules.AbstractAuthServletTask; import at.gv.egiz.eaaf.core.impl.utils.DataURLBuilder; import at.gv.egiz.eaaf.core.impl.utils.StreamUtils; import at.gv.egiz.eaaf.core.impl.utils.TransactionIDUtils; import at.gv.egovernment.moa.id.advancedlogging.MOAIDEventConstants; import at.gv.egovernment.moa.id.auth.modules.sl20_auth.Constants; import at.gv.egovernment.moa.id.auth.modules.sl20_auth.data.VerificationResult; import at.gv.egovernment.moa.id.auth.modules.sl20_auth.exceptions.SL20Exception; import at.gv.egovernment.moa.id.auth.modules.sl20_auth.exceptions.SL20SecurityException; import at.gv.egovernment.moa.id.auth.modules.sl20_auth.exceptions.SLCommandoParserException; import at.gv.egovernment.moa.id.auth.modules.sl20_auth.sl20.IJOSETools; import at.gv.egovernment.moa.id.auth.modules.sl20_auth.sl20.SL20Constants; import at.gv.egovernment.moa.id.auth.modules.sl20_auth.sl20.SL20JSONBuilderUtils; import at.gv.egovernment.moa.id.auth.modules.sl20_auth.sl20.SL20JSONExtractorUtils; import at.gv.egovernment.moa.id.commons.api.exceptions.MOAIDException; import at.gv.egovernment.moa.util.MiscUtil; import at.gv.egovernment.moaspss.logging.Logger; @Component("ReceiveQualeIDTask") public class ReceiveQualeIDTask extends AbstractAuthServletTask { @Autowired(required=true) private IJOSETools joseTools; @Override public void execute(ExecutionContext executionContext, HttpServletRequest request, HttpServletResponse response) throws TaskExecutionException { String sl20Result = null; try { Logger.debug("Receiving SL2.0 response process .... "); JsonObject sl20ReqObj = null; try { //get SL2.0 command or result from HTTP request Map reqParams = getParameters(request); sl20Result = reqParams.get(SL20Constants.PARAM_SL20_REQ_COMMAND_PARAM); if (MiscUtil.isEmpty(sl20Result)) { //Workaround for SIC Handy-Signature, because it sends result in InputStream String isReqInput = StreamUtils.readStream(request.getInputStream(), "UTF-8"); if (MiscUtil.isNotEmpty(isReqInput)) { Logger.info("Use SIC Handy-Signature work-around!"); sl20Result = isReqInput.substring("slcommand=".length()); } else { Logger.info("NO SL2.0 commando or result FOUND."); throw new SL20Exception("sl20.04", null); } } Logger.trace("Received SL2.0 result: " + sl20Result); revisionsLogger.logEvent(pendingReq, MOAIDEventConstants.AUTHPROCESS_BKU_DATAURL_IP, request.getRemoteAddr()); //parse SL2.0 command/result into JSON try { JsonParser jsonParser = new JsonParser(); JsonElement sl20Req = jsonParser.parse(Base64Url.decodeToUtf8String(sl20Result)); sl20ReqObj = sl20Req.getAsJsonObject(); } catch (JsonSyntaxException e) { Logger.warn("SL2.0 command or result is NOT valid JSON.", e); Logger.debug("SL2.0 msg: " + sl20Result); throw new SL20Exception("sl20.02", new Object[]{"SL2.0 command or result is NOT valid JSON."}, e); } //validate reqId with inResponseTo String sl20ReqId = pendingReq.getRawData(Constants.PENDING_REQ_STORAGE_PREFIX + SL20Constants.SL20_REQID, String.class); String inRespTo = SL20JSONExtractorUtils.getStringValue(sl20ReqObj, SL20Constants.SL20_INRESPTO, true); if (sl20ReqId == null || !sl20ReqId.equals(inRespTo)) { Logger.info("SL20 'reqId': " + sl20ReqId + " does NOT match to 'inResponseTo':" + inRespTo); throw new SL20SecurityException("SL20 'reqId': " + sl20ReqId + " does NOT match to 'inResponseTo':" + inRespTo); } //validate signature VerificationResult payLoadContainer = SL20JSONExtractorUtils.extractSL20PayLoad( sl20ReqObj, joseTools, authConfig.getBasicMOAIDConfigurationBoolean(Constants.CONFIG_PROP_FORCE_EID_SIGNED_RESULT, true)); if ( (payLoadContainer.isValidSigned() == null || !payLoadContainer.isValidSigned())) { if (authConfig.getBasicMOAIDConfigurationBoolean(Constants.CONFIG_PROP_FORCE_EID_SIGNED_RESULT, true)) { Logger.info("SL20 result from VDA was not valid signed"); throw new SL20SecurityException(new Object[]{"Signature on SL20 result NOT valid."}); } else { Logger.warn("SL20 result from VDA is NOT valid signed, but signatures-verification is DISABLED by configuration!"); } } /*TODO validate certificate by using MOA-SPSS * currently, the certificate is validated in IJOSETools by using a pkcs12 or jks keystore */ List sigCertChain = payLoadContainer.getCertChain(); //extract payloaf JsonObject payLoad = payLoadContainer.getPayload(); //check response type if (SL20JSONExtractorUtils.getStringValue( payLoad, SL20Constants.SL20_COMMAND_CONTAINER_NAME, true) .equals(SL20Constants.SL20_COMMAND_IDENTIFIER_QUALIFIEDEID)) { Logger.debug("Find " + SL20Constants.SL20_COMMAND_IDENTIFIER_QUALIFIEDEID + " result .... "); JsonElement qualeIDResult = SL20JSONExtractorUtils.extractSL20Result( payLoad, joseTools, authConfig.getBasicMOAIDConfigurationBoolean(Constants.CONFIG_PROP_FORCE_EID_ENCRYPTION, true)); //extract attributes from result Map eIDData = SL20JSONExtractorUtils.getMapOfStringElements(qualeIDResult); String idlB64 = eIDData.get(SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_IDL); String authBlockB64 = eIDData.get(SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_AUTHBLOCK); String ccsURL = eIDData.get(SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_CCSURL); String LoA = eIDData.get(SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_LOA); if (MiscUtil.isEmpty(idlB64) || MiscUtil.isEmpty(authBlockB64) || MiscUtil.isEmpty(LoA) || MiscUtil.isEmpty(ccsURL)) { Logger.info("SL20 'qualifiedeID' result does NOT contain all required attributes."); throw new SLCommandoParserException("SL20 'qualifiedeID' result does NOT contain all required attributes."); } //cache qualified eID data into pending request pendingReq.setRawDataToTransaction( Constants.PENDING_REQ_STORAGE_PREFIX + SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_IDL, idlB64); pendingReq.setRawDataToTransaction( Constants.PENDING_REQ_STORAGE_PREFIX + SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_AUTHBLOCK, authBlockB64); pendingReq.setRawDataToTransaction( Constants.PENDING_REQ_STORAGE_PREFIX + SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_CCSURL, ccsURL); pendingReq.setRawDataToTransaction( Constants.PENDING_REQ_STORAGE_PREFIX + SL20Constants.SL20_COMMAND_PARAM_EID_RESULT_LOA, LoA); } else { Logger.info("SL20 response is NOT a " + SL20Constants.SL20_COMMAND_IDENTIFIER_QUALIFIEDEID + " result"); throw new SLCommandoParserException("SL20 response is NOT a " + SL20Constants.SL20_COMMAND_IDENTIFIER_QUALIFIEDEID + " result"); } } catch (MOAIDException e) { Logger.warn("SL2.0 processing error:", e); if (sl20Result != null) Logger.debug("Received SL2.0 result: " + sl20Result); pendingReq.setRawDataToTransaction( Constants.PENDING_REQ_STORAGE_PREFIX + SL20Constants.SL20_COMMAND_IDENTIFIER_ERROR, new TaskExecutionException(pendingReq, "SL2.0 Authentication FAILED. Msg: " + e.getMessage(), e)); } catch (Exception e) { Logger.warn("ERROR:", e); Logger.warn("SL2.0 Authentication FAILED with a generic error.", e); if (sl20Result != null) Logger.debug("Received SL2.0 result: " + sl20Result); pendingReq.setRawDataToTransaction( Constants.PENDING_REQ_STORAGE_PREFIX + SL20Constants.SL20_COMMAND_IDENTIFIER_ERROR, new TaskExecutionException(pendingReq, e.getMessage(), e)); } finally { //store pending request requestStoreage.storePendingRequest(pendingReq); //write SL2.0 response if (sl20ReqObj != null) buildResponse(request, response, sl20ReqObj); else buildErrorResponse(request, response, "2000", "General transport Binding error"); } } catch (Exception e) { //write internal server errror 500 according to SL2.0 specification, chapter https transport binding Logger.warn("Can NOT build SL2.0 response. Reason: " + e.getMessage(), e); if (sl20Result != null) Logger.debug("Received SL2.0 result: " + sl20Result); try { response.sendError(500, "Internal Server Error."); } catch (IOException e1) { Logger.error("Can NOT send error message. SOMETHING IS REALY WRONG!", e); } } finally { TransactionIDUtils.removeTransactionId(); TransactionIDUtils.removeSessionId(); } } private void buildErrorResponse(HttpServletRequest request, HttpServletResponse response, String errorCode, String errorMsg) throws Exception { JsonObject error = SL20JSONBuilderUtils.createErrorCommandResult(errorCode, errorMsg); JsonObject respContainer = SL20JSONBuilderUtils.createGenericRequest( UUID.randomUUID().toString(), null, error , null); Logger.debug("Client request containts 'native client' header ... "); Logger.trace("SL20 response to VDA: " + respContainer); StringWriter writer = new StringWriter(); writer.write(respContainer.toString()); final byte[] content = writer.toString().getBytes("UTF-8"); response.setStatus(HttpServletResponse.SC_OK); response.setContentLength(content.length); response.setContentType(ContentType.APPLICATION_JSON.toString()); response.getOutputStream().write(content); } private void buildResponse(HttpServletRequest request, HttpServletResponse response, JsonObject sl20ReqObj) throws IOException, SL20Exception { //create response Map reqParameters = new HashMap(); reqParameters.put(EAAFConstants.PARAM_HTTP_TARGET_PENDINGREQUESTID, pendingReq.getPendingRequestId()); JsonObject callReqParams = SL20JSONBuilderUtils.createCallCommandParameters( new DataURLBuilder().buildDataURL(pendingReq.getAuthURL(), Constants.HTTP_ENDPOINT_RESUME, null), SL20Constants.SL20_COMMAND_PARAM_GENERAL_CALL_METHOD_GET, false, reqParameters); JsonObject callCommand = SL20JSONBuilderUtils.createCommand(SL20Constants.SL20_COMMAND_IDENTIFIER_CALL, callReqParams); //build first redirect command for app JsonObject redirectOneParams = SL20JSONBuilderUtils.createRedirectCommandParameters( generateICPRedirectURLForDebugging(), callCommand, null, true); JsonObject redirectOneCommand = SL20JSONBuilderUtils.createCommand(SL20Constants.SL20_COMMAND_IDENTIFIER_REDIRECT, redirectOneParams); //build second redirect command for IDP JsonObject redirectTwoParams = SL20JSONBuilderUtils.createRedirectCommandParameters( new DataURLBuilder().buildDataURL(pendingReq.getAuthURL(), Constants.HTTP_ENDPOINT_RESUME, null), redirectOneCommand, null, true); JsonObject redirectTwoCommand = SL20JSONBuilderUtils.createCommand(SL20Constants.SL20_COMMAND_IDENTIFIER_REDIRECT, redirectTwoParams); //build generic SL2.0 response container String transactionId = SL20JSONExtractorUtils.getStringValue(sl20ReqObj, SL20Constants.SL20_TRANSACTIONID, false); JsonObject respContainer = SL20JSONBuilderUtils.createGenericRequest( UUID.randomUUID().toString(), transactionId, redirectTwoCommand, null); //workaround for A-Trust if (request.getHeader(SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE) != null && request.getHeader(SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE).equals(SL20Constants.HTTP_HEADER_VALUE_NATIVE) || true) { Logger.debug("Client request containts 'native client' header ... "); Logger.trace("SL20 response to VDA: " + respContainer); StringWriter writer = new StringWriter(); writer.write(respContainer.toString()); final byte[] content = writer.toString().getBytes("UTF-8"); response.setStatus(HttpServletResponse.SC_OK); response.setContentLength(content.length); response.setContentType(ContentType.APPLICATION_JSON.toString()); response.getOutputStream().write(content); } else { Logger.info("SL2.0 DataURL communication needs http header: '" + SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE + "'"); throw new SL20Exception("sl20.06", new Object[] {"SL2.0 DataURL communication needs http header: '" + SL20Constants.HTTP_HEADER_SL20_CLIENT_TYPE + "'"}); } } /** * Generates a IPC redirect URL that is configured on IDP side * * @return IPC ReturnURL, or null if no URL is configured */ private String generateICPRedirectURLForDebugging() { final String PATTERN_PENDING_REQ_ID = "#PENDINGREQID#"; String ipcRedirectURLConfig = authConfig.getBasicConfiguration(Constants.CONFIG_PROP_IPC_RETURN_URL); if (MiscUtil.isNotEmpty(ipcRedirectURLConfig)) { if (ipcRedirectURLConfig.contains(PATTERN_PENDING_REQ_ID)) { Logger.trace("Find 'pendingReqId' pattern in IPC redirect URL. Update url ... "); ipcRedirectURLConfig = ipcRedirectURLConfig.replaceAll( "#PENDINGREQID#", EAAFConstants.PARAM_HTTP_TARGET_PENDINGREQUESTID + "=" + pendingReq.getPendingRequestId()); } return ipcRedirectURLConfig; } return null; } }