diff --git a/agama/misc/json_crash.ftl b/agama/misc/json_crash.ftlh similarity index 100% rename from agama/misc/json_crash.ftl rename to agama/misc/json_crash.ftlh diff --git a/agama/misc/json_finished.ftl b/agama/misc/json_finished.ftlh similarity index 100% rename from agama/misc/json_finished.ftl rename to agama/misc/json_finished.ftlh diff --git a/agama/misc/json_mismatch.ftl b/agama/misc/json_mismatch.ftlh similarity index 100% rename from agama/misc/json_mismatch.ftl rename to agama/misc/json_mismatch.ftlh diff --git a/agama/misc/json_template.ftl b/agama/misc/json_template.ftlh similarity index 100% rename from agama/misc/json_template.ftl rename to agama/misc/json_template.ftlh diff --git a/agama/misc/json_timeout.ftl b/agama/misc/json_timeout.ftlh similarity index 100% rename from agama/misc/json_timeout.ftl rename to agama/misc/json_timeout.ftlh diff --git a/docs/script-catalog/person_authentication/agama-bridge/AgamaBridge.py b/docs/script-catalog/person_authentication/agama-bridge/AgamaBridge.py index 212c1a2ab5e..ebbfaa433c5 100644 --- a/docs/script-catalog/person_authentication/agama-bridge/AgamaBridge.py +++ b/docs/script-catalog/person_authentication/agama-bridge/AgamaBridge.py @@ -144,7 +144,7 @@ def prepareForStep(self, configurationAttributes, requestParameters, step): try: bridge = CdiUtil.bean(NativeJansFlowBridge) - running = bridge.prepareFlow(session.getId(), qn, ins) + running = bridge.prepareFlow(session.getId(), qn, ins, False) if running == None: print "Agama. Flow '%s' does not exist or cannot be launched from a browser!" % qn diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/NativeJansFlowBridge.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/NativeJansFlowBridge.java index 6df66696dfe..c334e4ce8f2 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/NativeJansFlowBridge.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/NativeJansFlowBridge.java @@ -42,7 +42,7 @@ public String getTriggerUrl() { "agama" + ExecutionServlet.URL_SUFFIX; } - public Boolean prepareFlow(String sessionId, String qname, String jsonInput) throws Exception { + public Boolean prepareFlow(String sessionId, String qname, String jsonInput, boolean nativeClient) throws Exception { logger.info("Preparing flow '{}'", qname); Boolean alreadyRunning = null; @@ -68,6 +68,7 @@ public Boolean prepareFlow(String sessionId, String qname, String jsonInput) thr st.setQname(qname); st.setJsonInput(jsonInput); st.setFinishBefore(expireAt); + st.setNativeClient(nativeClient); aps.createFlowRun(sessionId, st, expireAt); LogUtils.log("@w Effective timeout for this flow will be % seconds", timeout); } diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/model/FlowStatus.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/model/FlowStatus.java index dee36df5b4f..6592cf5037a 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/model/FlowStatus.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/model/FlowStatus.java @@ -16,6 +16,7 @@ public class FlowStatus { private String templatePath; private long startedAt; private long finishBefore; + private boolean nativeClient; @JsonInclude(JsonInclude.Include.NON_NULL) private Object templateDataModel; @@ -59,6 +60,14 @@ public void setFinishBefore(long finishBefore) { this.finishBefore = finishBefore; } + public boolean isNativeClient() { + return nativeClient; + } + + public void setNativeClient(boolean nativeClient) { + this.nativeClient = nativeClient; + } + public String getQname() { return qname; } diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/page/Page.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/page/Page.java index 81513870f3f..a7956316ccd 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/page/Page.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/page/Page.java @@ -6,9 +6,7 @@ import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; - -import java.util.HashMap; -import java.util.Map; +import java.util.*; import io.jans.agama.engine.service.*; import io.jans.service.CacheService; @@ -36,7 +34,6 @@ public class Page { private String templatePath; private Map dataModel; - private Object rawModel; public String getTemplatePath() { return templatePath; @@ -47,42 +44,28 @@ public void setTemplatePath(String templatePath) { } public Object getDataModel() { + return dataModel; + } + + public Object getAugmentedDataModel(boolean includeContextualData, Map extra) { - if (rawModel == null) { - if (dataModel != null) { - - dataModel.putIfAbsent(WEB_CTX_KEY, webContext); - dataModel.putIfAbsent(MessagesService.BUNDLE_ID, msgsService); - dataModel.putIfAbsent(LabelsService.METHOD_NAME, labelsService); - dataModel.putIfAbsent(CACHE_KEY, cache); - return dataModel; - - } else return new Object(); - } else return rawModel; + Map model = new HashMap<>(dataModel); + + if (includeContextualData) { + model.putIfAbsent(WEB_CTX_KEY, webContext); + model.putIfAbsent(MessagesService.BUNDLE_ID, msgsService); + model.putIfAbsent(LabelsService.METHOD_NAME, labelsService); + model.putIfAbsent(CACHE_KEY, cache); + } + if (extra != null) { + extra.forEach((k, v) -> model.putIfAbsent(k, v)); + } + return model; - } - - /** - * This call is cheaper than setDataModel, but pages won't have access to any - * contextual data - * @param object - */ - public void setRawDataModel(Object object) { - rawModel = object; - dataModel = null; } public void setDataModel(Object object) { - rawModel = null; - dataModel = mapFromObject(object); - } - - public void appendToDataModel(Object object) { - if (rawModel != null) { - rawModel = null; - dataModel = new HashMap<>(); - } - dataModel.putAll(mapFromObject(object)); + dataModel = object == null ? Map.of() : mapFromObject(object); } private Map mapFromObject(Object object) { @@ -91,7 +74,7 @@ private Map mapFromObject(Object object) { @PostConstruct private void init() { - dataModel = new HashMap<>(); + dataModel = Map.of(); } } diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/AgamaPersistenceService.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/AgamaPersistenceService.java index d9f3641af58..89e95632222 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/AgamaPersistenceService.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/AgamaPersistenceService.java @@ -189,7 +189,7 @@ public void saveState(String sessionId, FlowStatus fst, NativeContinuation conti logger.debug("Saving state of current flow run"); entryManager.merge(run); - + } public void finishFlow(String sessionId, FlowResult result) throws IOException { diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/FlowService.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/FlowService.java index dcee5e9e686..1ad8494bfed 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/FlowService.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/FlowService.java @@ -51,7 +51,7 @@ public class FlowService { private static final String SESSION_ID_COOKIE = "session_id"; private static final String SCRIPT_SUFFIX = ".js"; - private static final int TIMEOUT_SKEW = 8000; //millisecons + private static final int TIMEOUT_SKEW = 8000; //milliseconds @Inject private Logger logger; @@ -272,6 +272,9 @@ private FlowStatus processPause(ContinuationPending pending, FlowStatus status) } else if (pending instanceof PendingRedirectException) { + if (status.isNativeClient()) + throw new IOException("RFAC for native clients is not available"); + PendingRedirectException pre = (PendingRedirectException) pending; status.setTemplatePath(null); diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/BaseServlet.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/BaseServlet.java index 4393a0870dc..873ba2e6abb 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/BaseServlet.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/BaseServlet.java @@ -1,5 +1,8 @@ package io.jans.agama.engine.servlet; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + import io.jans.agama.engine.exception.TemplateProcessingException; import io.jans.agama.engine.misc.FlowUtils; import io.jans.agama.engine.page.BasicTemplateModel; @@ -13,13 +16,18 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.core.MediaType; +import java.util.Map; import java.io.IOException; import java.io.StringWriter; import org.slf4j.Logger; +import static java.nio.charset.StandardCharsets.UTF_8; + public abstract class BaseServlet extends HttpServlet { + private static final String TEMPLATE_PATH_KEY = "_template"; + @Inject protected Logger logger; @@ -29,6 +37,9 @@ public abstract class BaseServlet extends HttpServlet { @Inject private TemplatingService templatingService; + @Inject + private ObjectMapper mapper; + @Inject protected EngineConfig engineConf; @@ -49,6 +60,8 @@ protected void sendNotAvailable(HttpServletResponse response) throws IOException protected void sendFlowTimeout(HttpServletResponse response, String message) throws IOException { + response.setStatus(HttpServletResponse.SC_GONE); + String errorPage = engineConf.getInterruptionErrorPage(); page.setTemplatePath(errorPath(errorPage)); page.setDataModel(new BasicTemplateModel(message)); @@ -62,7 +75,7 @@ protected void sendFlowCrashed(HttpServletResponse response, String error) throw String errorPage = engineConf.getCrashErrorPage(); page.setTemplatePath(errorPath(errorPage)); - page.setRawDataModel(new BasicTemplateModel(error)); + page.setDataModel(new BasicTemplateModel(error)); sendPageContents(response); } @@ -79,11 +92,22 @@ protected void sendPageMismatch(HttpServletResponse response, String message, St } - protected void sendPageContents(HttpServletResponse response) throws IOException { + protected void sendPageContents(HttpServletResponse response) throws IOException { + sendPageContents(response, false); + } + + protected void sendPageContents(HttpServletResponse response, boolean nativeClient) throws IOException { try { - processTemplate(response, page.getTemplatePath(), page.getDataModel()); - } catch (TemplateProcessingException e) { + if (nativeClient) { + String simplePath = shortenPath(page.getTemplatePath(), 2); + Object model = page.getAugmentedDataModel(false, Map.of(TEMPLATE_PATH_KEY, simplePath)); + String entity = mapper.writeValueAsString(model); + processResponse(response, UTF_8.toString(), MediaType.APPLICATION_JSON, entity); + } else { + processTemplate(response, page.getTemplatePath(), page.getAugmentedDataModel(true, null)); + } + } catch (TemplateProcessingException | JsonProcessingException e) { try { response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); @@ -100,23 +124,41 @@ protected void sendPageContents(HttpServletResponse response) throws IOException private String errorPath(String page) { return isJsonRequest() ? engineConf.getJsonErrorPage(page) : page; } - + private void processTemplate(HttpServletResponse response, String path, Object dataModel) throws TemplateProcessingException, IOException { StringWriter sw = new StringWriter(); Pair contentType = templatingService.process(path, dataModel, sw, false); - - //encoding MUST be set before calling getWriter - response.setCharacterEncoding(contentType.getSecond()); + processResponse(response, contentType.getSecond(), contentType.getFirst(), sw.toString()); + } + + private void processResponse(HttpServletResponse response, String charset, String mediaType, + String entity) throws IOException { + + //encoding MUST be set before calling getWriter + response.setCharacterEncoding(charset); engineConf.getDefaultResponseHeaders().forEach((h, v) -> response.setHeader(h, v)); - String mediaType = contentType.getFirst(); + if (mediaType != null) { response.setContentType(mediaType); } - response.getWriter().write(sw.toString()); + response.getWriter().write(entity); } + private String shortenPath(String str, int subPaths) { + + int idx = (str.charAt(0) == '/') ? 1 : 0; + + for (int i = 0; i < subPaths; i++) { + int j = str.indexOf("/", idx); + if (j == -1) break; + idx = j + 1; + } + return str.substring(idx); + + } + } diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/ExecutionServlet.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/ExecutionServlet.java index 6a874625994..f6a91b240df 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/ExecutionServlet.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/ExecutionServlet.java @@ -71,7 +71,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) if (path.equals(expectedUrl)) { page.setTemplatePath(engineConf.getTemplatesPath() + "/" + fstatus.getTemplatePath()); page.setDataModel(fstatus.getTemplateDataModel()); - sendPageContents(response); + sendPageContents(response, fstatus.isNativeClient()); } else { //This is an attempt to GET a page which is not the current page of this flow //json-based clients must explicitly pass the content-type in GET requests @@ -194,6 +194,7 @@ private void sendRedirect(HttpServletResponse response, String contextPath, Flow // Local redirection newLocation = contextPath + getExpectedUrl(fls); } + //See https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections and //https://stackoverflow.com/questions/4764297/difference-between-http-redirect-codes if (currentIsGet) {