From 856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 Mon Sep 17 00:00:00 2001 From: Jose Gonzalez Date: Fri, 10 Jan 2025 08:02:37 -0500 Subject: [PATCH] feat: allow integration of Agama flows into the authz challenge enpoint (#10587) * docs: add relevant documentation #10460 Signed-off-by: jgomer2001 * feat: add supporting code for Challenge script #10460 Signed-off-by: jgomer2001 * feat: add Challenge script #10460 Signed-off-by: jgomer2001 --------- Signed-off-by: jgomer2001 --- .../{json_template.ftlh => json_template.ftl} | 0 docs/assets/agama/challenge-flow.png | Bin 0 -> 81409 bytes docs/janssen-server/developer/agama/faq.md | 10 +- .../developer/agama/native-applications.md | 450 ++++++++++++++++++ .../AgamaChallenge.java | 317 ++++++++++++ jans-auth-server/agama/engine/pom.xml | 14 +- .../io/jans/agama/NativeJansFlowBridge.java | 2 +- .../jans/agama/engine/client/MiniBrowser.java | 211 ++++++++ .../service/AgamaPersistenceService.java | 12 +- .../java/io/jans/agama/test/BaseTest.java | 4 +- .../agama/test/CustomConfigsFlowTest.java | 4 +- .../jans_setup/templates/scripts.ldif | 15 + mkdocs.yml | 1 + 13 files changed, 1021 insertions(+), 19 deletions(-) rename agama/misc/{json_template.ftlh => json_template.ftl} (100%) create mode 100644 docs/assets/agama/challenge-flow.png create mode 100644 docs/janssen-server/developer/agama/native-applications.md create mode 100644 docs/script-catalog/authorization_challenge/AgamaChallenge.java create mode 100644 jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/client/MiniBrowser.java diff --git a/agama/misc/json_template.ftlh b/agama/misc/json_template.ftl similarity index 100% rename from agama/misc/json_template.ftlh rename to agama/misc/json_template.ftl diff --git a/docs/assets/agama/challenge-flow.png b/docs/assets/agama/challenge-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..9cb8292887b8882827fb61626435ca60d9a77d39 GIT binary patch literal 81409 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%0DgeAip2q+uXVAhfC2vGbyZK^zG* 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 KrHiEu1O5lwidcLA literal 0 HcmV?d00001 diff --git a/docs/janssen-server/developer/agama/faq.md b/docs/janssen-server/developer/agama/faq.md index f5b763206c1..c7f85d29cf5 100644 --- a/docs/janssen-server/developer/agama/faq.md +++ b/docs/janssen-server/developer/agama/faq.md @@ -129,12 +129,6 @@ We plan to offer a debugger in the future. In the meantime, you can do `printf`- ## Miscellaneous -### Does the engine support AJAX? - -If you require a flow with no page refreshes, it could be implemented using AJAX calls as long as they align to the [POST-REDIRECT-GET](./advanced-usages.md#flow-advance-and-navigation) pattern, where a form is submitted, and as response a 302/303 HTTP redirection is obtained. Your Javascript code must also render UI elements in accordance with the data obtained by following the redirect (GET). Also, care must be taken in order to process server errors, timeouts, etc. In general, this requires a considerable amount of effort. - -If you require AJAX to consume a resource (service) residing in the same domain of your server, there is no restriction - the engine is not involved. Interaction with external domains may require to setup CORS configuration appropriately in the authentication server. - ### How to launch a flow? A flow is launched by issuing an authentication request in a browser as explained [here](./jans-agama-engine.md#launching-flows). @@ -195,3 +189,7 @@ Note the localization context (language, country, etc.) used in such a call is b ### Can Agama code be called from Java? No. These two languages are supposed to play roles that should not be mixed, check [here](./agama-best-practices.md#about-flow-design). + +### How to run flows from native applications instead of web browsers? + +There is a separate doc page covering this aspect [here](./native-applications.md). diff --git a/docs/janssen-server/developer/agama/native-applications.md b/docs/janssen-server/developer/agama/native-applications.md new file mode 100644 index 00000000000..4cc4831d2ae --- /dev/null +++ b/docs/janssen-server/developer/agama/native-applications.md @@ -0,0 +1,450 @@ +--- +tags: + - developer + - agama + - native apps + - challenge endpoint +--- + +# Agama flows in native applications + +Agama is a framework primarily focused on web flows, however, with the [Authorization Challenge](../../../script-catalog/authorization_challenge/authorization-challenge.md) endpoint of Jans Server, developers can now run their flows outside the browser. This makes possible to offer secure, multi-step authentication flows from desktop and mobile applications without resorting to mechanisms like Web Views that degrade the user experience substantially. + +Additionally, the same already-familiar tools for authoring and deploying Agama projects can be used for the job. Moreover, the flows built for the web can be run in the native world without modification, requiring only to code the respective native UI and the logic that interacts with the Authorization Challenge endpoint, called "the endpoint" hereafter. + +In this document, we present an overview on how the endpoint works to make your Agama flows run without a web browser. Preliminar acquaintance with the following topics is recommended: + +- Agama [DSL](../../../agama/introduction.md#dsl) and `.gama` [format](../../../agama/gama-format.md) +- Agama projects [deployment](../../../config-guide/auth-server-config/agama-project-configuration.md) in the Janssen Server +- [Execution rules](../../../agama/execution-rules.md) in the Jans Agama [engine](../../../agama/jans-agama-engine.md) +- A basic understanding of [OAuth 2.0 for First-Party Applications](https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html) + +## How do flows actually run? + +Before getting into the technicalities, let's cover some key preliminar concepts. + +The engine - the piece of software that actually runs flows - is eminently driven by HTTP requests. This is unsurprising because the main "consumers" of the engine are web browsers. When targetting native apps, the engine remains the same, and flows still run at the server side. This means native apps won't hold any business logic, or make computations of significance. + +The [RRF](../../../agama/language-reference.md#rrf) (render-reply-fetch) Agama instruction is of paramount importance in flows. In a regular web setting, it involves three steps: + +- Injecting some data to a UI template in order to generate HTML markup. This is known as _rendering_ +- Reply the markup to the web browser - this will display a web page +- At the server side, retrieve data the user may have provided in his interaction with the page. This is, _fetch_ + +In a native setting no HTML markup is suppossed to be generated and replied - it's the app that is in charge of displaying the UI now. For this purpose, it will receive (from the endpoint) the data that would be originally injected into the template. Most of times, this will carry information gathered at earlier stages of the flow and that is relevant to properly show or update the UI. + +Likewise, the "data submission" for the _fetch_ phase of RRF is performed by the app too. In this case, the relevant data grabbed from the user interaction is sent to the server side (via challenge endpoint) and becomes the result of the RRF (the value for the variable on the left-hand side of the instruction). Note both the input ("injected" data) and the output (result) is specified in JSON format. + +Once the _fetch_ occurs, the flow proceeds its execution as normal until another RRF instruction is hit, where the procedure described above takes place again. + +Note this approach has two big benefits: + +1. Regular web flows can be reused in the native world without modifications +1. The mindset for flows design remain the same + +There is a subtle exception regarding the first statement and has to do with flows containing RFAC instructions. [RFAC](../../../agama/language-reference.md#rfac) is used to redirect to external sites, and as such, it requires a web browser. In the case of native apps, flows will crash once an RFAC instruction is hit. + +### Inversion of control in apps + +The above concepts bring an important constraint to app design that should be accounted before undertaking any project: control is inverted. + +Normally, an app "knows" exactly what to do at each step of its workflow, and eventually delegates data retrieval tasks to the server side. When using the endpoint, the server side drives the logic: the app does not "take decisions" and instead "reacts" to the received data. This will be demostrated later through a practical example. + +## About the example: OTP via e-mail + +To avoid a rather abstract explanation, we'll use an example to illustrate the steps required to run a flow from a native app. Suppose an authentication flow operating in the following manner: + +- A username is prompted +- If the corresponding user has no e-mail associated to his account, the flow ends with an error message +- If the user has exactly one e-mail in his profile, a random one-time passcode (OTP) is sent to his registered address +- If the user has more than one e-mail, a screen is shown to pick the address where he would like the OTP be sent to +- The user is prompted to enter the passcode sent. If supplied correctly, the flow ends and the user is authenticated, otherwise the flow ends with an error + +This hypothetical flow is simple but will give you a good idea on how to interact with the endpoint. + +### The flow code + +The following code depicts the implementation. + +![enabled-2fa-methods](../../assets/agama/challenge-flow.png) + +Flow `co.acme.flows.emailOtp` is self-explanatory and does not require further insight. Note the templates referenced (in RRF directives) don't necessarily have to exist, however, the template names will be included in the output of the endpoint as the flow executes. This serves as a hint or reference for the app to know the current point of execution and determine what should be shown in the UI. It will be more clearly seen in the next section. + +## Running the flow + +### Requisites + +To be able to run an Agama flow from a native app using the endpoint, it is required to register an OAuth Client in the Jans server with at least the `authorization_challenge` scope. The process of client registration is beyond the scope of this document. + +All HTTP requests exemplified here make use of `curl`. Ensure this tool is familiar to you. + +### Workflow + +Requests to the endpoint are all issued to the URL `https:///jans-auth/restv1/authorize-challenge` using the POST verb. Responses will contain JSON content whose structure will vary depending on the result of the operation as we will see. + +Once the first request is sent, the flow will start and all instructions will be executed until an RRF is found. Here the flow will be paused, and the endpoint will respond with the data that was passed to RRF: the template path and the "injected" data. Let's start issuing real requests now. + +### Initial request + +In the first request, at least the following parameters must be passed: + +|Name|Value| +|-|-| +|`acr_values`|agama_challenge| +|`use_auth_session`|true| +|`client_id`|The client identifier of a previously registered client| +|`flow_name`|The qualified name of the flow to launch| + +So in our example, it may look like: + +``` +curl -i -d acr_values=agama_challenge -d use_auth_session=true + -d flow_name=co.acme.flows.emailOtp -d client_id= + https:///jans-auth/restv1/authorize-challenge +``` + +!!! Note + This command, as all others following has been split into several lines for better readability. + +The response will look like: + +``` +HTTP/1.1 401 Unauthorized +Content-Type: application/json +... + +{ + "error": "flow_paused" + "flow_paused": { + "_template": "username-prompt.ftl" + }, + "auth_session": "BmAiCeArLdAa0", +} +``` + +While this may look like something wrong happened, it is not really the case. This is derived from the spec the endpoint adheres to, where the authorization server must report every intermediate response as an error with a 401 status code. + +The value of the `error` property references a section that contains the template path. Here it corresponds to the first RRF instruction reached in the execution (line 4 in the flow's code). Particularly this RRF does not have a second parameter, so there is only one property inside the `flow_paused` JSON object. + +Note the presence of `auth_session`. This value allows the authorization server to associate subsequent requests issued by the app with this specific flow execution. + +Based on this response, the app should render UI elements in order to capture the username. Here, `username-prompt.ftl` serves as a hint for the app to know the point of execution the flow is at currently. + +### Subsequent requests + +From here onwards, requests must contain the following parameters: + +|Name|Value| +|-|-| +|`use_auth_session`|true| +|`auth_session`|The value obtained in the initial request| +|`data`|A JSON object value which will become the result of the RRF instruction the flow is paused at| + +!!! Note: + Whenever a request is missing the `auth_session` param, it is assumed the [inital request](#initial-request) is being attempted. + +Let's assume the user entered `Joan` as username in the app. A request like the below can then be issued so the variable `obj` at line 4 is assigned a value: + +``` +curl -i -d auth_session=BmAiCeArLdAa0 -d use_auth_session=true + --data-urlencode data='{ "username": "Joan" }' + https:///jans-auth/restv1/authorize-challenge +``` + +This will make the flow advance until the next RRF is reached. Suppose the user Joan was found to have two e-mail addresses: `joan@doe.com` and `joan@deere.com`. This will make the flow hit line 23. The response will look as follows: + +``` +HTTP/1.1 401 Unauthorized +Content-Type: application/json +... + +{ + "error": "flow_paused" + "flow_paused": { + "_template": "email-prompt.ftl", + "addresses": [ "joan@doe.com", "joan@deere.com" ] + }, + "auth_session": "BmAiCeArLdAa0", +} +``` + +Note the `flow_paused` section has the contents of the object prepared in line 22. + +Based on this response, now the app should show a selection list for the user to pick one of these addresses. Once the selection is made, a new request can be issued: + +``` +curl -i -d auth_session=BmAiCeArLdAa0 -d use_auth_session=true + --data-urlencode data='{ "email": "joan@doe.com" }' + https:///jans-auth/restv1/authorize-challenge +``` + +The flow will continue and the hypothetical message will be sent to `joan@doe.com` (line 28). Then the next RRF is reached (line 32) and we get as response: + +``` +HTTP/1.1 401 Unauthorized +Content-Type: application/json +... + +{ + "error": "flow_paused" + "flow_paused": { + "_template": "passcode-prompt.ftl" + }, + "auth_session": "BmAiCeArLdAa0", +} +``` + +The app must now update the UI so the passcode is prompted. When ready, a new request comes: + +``` +curl -i -d auth_session=BmAiCeArLdAa0 -d use_auth_session=true + --data-urlencode data='{ "otp": "123456" }' + https:///jans-auth/restv1/authorize-challenge +``` + +Assuming the entered code (123456) was correct, the response would look like: + +``` +HTTP/1.1 401 Unauthorized +Content-Type: application/json +... + +{ + "error": "flow_finished", + "flow_finished": { + "data": { "userId": "Joan" }, + "success": true + }, + "auth_session": "efb10525-6c43-4e50-88ab-92461c258526" +} +``` + +This means we have hit line 37. + +When a `Finish` instruction is reached it is fully executed and the error reported changes to `flow_finished`. What is left now is binding the user identified by `userId` (Joan) to the authorization request we have been handling (`BmAiCeArLdAa0`). This is how the user actually gets authenticated. + +### Final request + +To authenticate the user, we issue one last request: + +``` +curl -i -d auth_session=BmAiCeArLdAa0 -d use_auth_session=true + https:///jans-auth/restv1/authorize-challenge +``` + +Note parameter `data` is not needed. As response we obtain: + +``` +HTTP/1.1 200 OK +Content-Type: application/json +... + +{ "authorization_code" : "SplxlOBeZQQYbYS6WxSbIA" } + +``` + +Once an authorization code has been obtained, the app can request an access token. This topic is beyond the scope of this document. + +At this point, the app can update the UI giving the user access to the actual app contents. No more requests are expected to be received by the endpoint with the given `auth_session` value. + +## Understanding errors + +So far we have been following the "happy" path in the example flow where all assumptions are met. This is unrealistic so here we offer an overview of how the endpoint behaves when abnormal conditions come up. + +!!! Note: + In this section, we stick to the terminology found [here](../../../agama/execution-rules.md#flows-lifecycle). + +### Missing parameters + +Assume the following request is issued: + +``` +curl -i -d use_auth_session=true -d acr_values=agama_challenge -d client_id= + https:///jans-auth/restv1/authorize-challenge +``` + +This lacks the name of the flow to launch. The response is: + +``` +HTTP/1.1 400 Bad Request +Content-Type: application/json +... + +{ + "error": "missing_param", + "missing_param": { "description": "Parameter 'flow_name' missing in request" } +} +``` + +### Failed flows + +Many times, flows simply fail as a way to reject access. This is achived in Agama by using code like: + +``` +obj = { success: false, error: "You are too suspicious" } +Finish obj +``` + +In this case, the response looks like: + +``` +HTTP/1.1 401 Unauthorized +Content-Type: application/json +... + +{ + "error": "flow_finished", + "flow_finished": { + "success": false, + "error": "You are too suspicious" + } +} +``` + +Note `auth_session` is not replied. As such, no more requests to the endpoint should be made passing an `auth_session` value obtained earlier. + +### Engine errors + +There are several conditions under which the engine produces errors. In these cases, the HTTP error emitted by the engine is included in the endpoint response. As in previous error scenarios, no `auth_session` is replied. + +#### Flow timeout + +With native apps, [timeout](./jans-agama-engine.md#how-timeouts-work) of flows obeys the same rules of the web scenario. The only difference is the server property employed for the timeout calculation, namely, `authorizationChallengeSessionLifetimeInSeconds`. If absent, it defaults to one day. + +Here is how a flow timeout is reported: + +``` +HTTP/1.1 500 Server Error +Content-Type: application/json +... + +{ + "error": "engine_error", + "engine_error": { + "description": "Unexpected response to https:///jans-auth/fl/...", + "body": { + "message": "You have exceeded the amount of time required to complete your authentication", + "timeout": true + }, + "contentType": "application/json", + "status": 410 + } +} +``` + +#### Crashed flow + +When a flow crashes, the error is reported in similar way the timeout is reported. Here are some examples: + +1. An attempt to access a property or index of a `null` variable in Agama code + +``` +HTTP/1.1 500 Server Error +Content-Type: application/json +... + +{ + "error": "engine_error", + "engine_error": { + "description": "Unexpected response to https:///jans-auth/fl/...", + "body": { + "title": "An unexpected error ocurred", + "message": "TypeError: Cannot read property \"x\" from null" + }, + "contentType": "application/json", + "status": 500 + } +} +``` + +2. A variable does not meet the expected shape for a given Agama directive + +``` +HTTP/1.1 500 Server Error +Content-Type: application/json +... + +{ + "error": "engine_error", + "engine_error": { + "description": "Unexpected response to https:///jans-auth/fl/...", + "body": { + "title": "An unexpected error ocurred", + "message": "TypeError: Data passed to RRF was not a map or Java equivalent" + }, + "contentType": "application/json", + "status": 500 + } +} +``` + +3. Indexing a string in Java beyond length + +``` +HTTP/1.1 500 Server Error +Content-Type: application/json +... + +{ + + "error": "engine_error", + "engine_error": { + "description": "Unexpected response to https:///jans-auth/fl/...", + "body": { + "title": "An unexpected error ocurred", + "message": "String index out of range: 100" + }, + "contentType": "application/json", + "status": 500 + } +} +``` + +### Other errors + +There are a variety of miscelaneous errors. Here we describe the most common. + +#### Finished flows with problems of user identification + +When a `Finish` instruction does not include a reference to a user identifier, or if the referenced user does not exist, the endpoint responds like: + +``` +HTTP/1.1 500 Server Error +Content-Type: application/json +... + +{ + "error": "unexpected_error", + "unexpected_error": { "description": "Unable to determine identity of user" } +} +``` + +#### Attempt to launch an unknown flow + +If the initial request references an inexisting flow or one that has been flagged as [not launchable directly](../../../agama/gama-format.md#metadata) by clients. + +``` +HTTP/1.1 500 Server Error +Content-Type: application/json +... + +{ + "unexpected_error": {"description": "Flow ... does not exist or cannot be launched an application"}, + "error": "unexpected_error" +} +``` + +#### Agama is disabled + +If the Agama engine is disabled, the following is generated upon the first request: + +``` +HTTP/1.1 500 Server Error +Content-Type: application/json +... + +{ + "error": "unexpected_error", + "unexpected_error": { "description": "Agama engine is disabled" } +} +``` diff --git a/docs/script-catalog/authorization_challenge/AgamaChallenge.java b/docs/script-catalog/authorization_challenge/AgamaChallenge.java new file mode 100644 index 00000000000..89fdade3277 --- /dev/null +++ b/docs/script-catalog/authorization_challenge/AgamaChallenge.java @@ -0,0 +1,317 @@ +import io.jans.as.common.model.common.User; +import io.jans.as.common.model.session.AuthorizationChallengeSession; +import io.jans.as.server.authorize.ws.rs.AuthorizationChallengeSessionService; +import io.jans.as.server.service.UserService; +import io.jans.as.server.service.external.context.ExternalScriptContext; +import io.jans.model.SimpleCustomProperty; +import io.jans.model.custom.script.model.CustomScript; +import io.jans.model.custom.script.type.authzchallenge.AuthorizationChallengeType; +import io.jans.orm.PersistenceEntryManager; +import io.jans.service.cdi.util.CdiUtil; +import io.jans.service.custom.script.CustomScriptManager; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.jans.agama.engine.model.*; +import io.jans.agama.engine.misc.FlowUtils; +import io.jans.agama.engine.service.AgamaPersistenceService; +import io.jans.agama.NativeJansFlowBridge; +import io.jans.agama.engine.client.MiniBrowser; +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.util.Base64Util; +import io.jans.util.*; + +import jakarta.servlet.ServletRequest; +import java.io.IOException; +import java.util.*; + +import org.json.*; + +import static io.jans.agama.engine.client.MiniBrowser.Outcome.*; + +public class AuthorizationChallenge implements AuthorizationChallengeType { + + //private static final Logger log = LoggerFactory.getLogger(AuthorizationChallenge.class); + private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class); + + private String finishIdAttr; + private MiniBrowser miniBrowser; + private PersistenceEntryManager entryManager; + private AuthorizationChallengeSessionService deviceSessionService; + + private boolean makeError(ExternalScriptContext context, AuthorizationChallengeSession deviceSessionObject, + boolean doRemoval, String errorId, JSONObject error, int status) { + + JSONObject jobj = new JSONObject(); + if (deviceSessionObject != null) { + + if (doRemoval) { + entryManager.remove(deviceSessionObject.getDn(), AuthorizationChallengeSession.class); + } else { + jobj.put("auth_session", deviceSessionObject.getId()); + } + } + + String errId = errorId.toLowerCase(); + jobj.put("error", errId); + jobj.put(errId, error); + + context.createWebApplicationException(status, jobj.toString(2) + "\n"); + return false; + + } + + private boolean makeUnexpectedError(ExternalScriptContext context, AuthorizationChallengeSession deviceSessionObject, + String description) { + + JSONObject jobj = new JSONObject(Map.of("description", description)); + return makeError(context, deviceSessionObject, true, "unexpected_error", jobj, 500); + + } + + private boolean makeMissingParamError(ExternalScriptContext context, String description) { + + JSONObject jobj = new JSONObject(Map.of("description", description)); + return makeError(context, null, false, "missing_param", jobj, 400); + + } + + private Pair prepareFlow(String sessionId, String flowName) { + + String msg = null; + try { + String qn = null, inputs = null; + + int i = flowName.indexOf("-"); + if (i == -1) { + qn = flowName; + } else if (i == 0) { + msg = "Flow name is empty"; + } else { + qn = flowName.substring(0, i); + scriptLogger.info("Parsing flow inputs"); + inputs = Base64Util.base64urldecodeToString(flowName.substring(i + 1)); + } + + if (qn != null) { + NativeJansFlowBridge bridge = CdiUtil.bean(NativeJansFlowBridge.class); + Boolean running = bridge.prepareFlow(sessionId, qn, inputs, true); + + if (running == null) { + msg = "Flow " + qn + " does not exist or cannot be launched from an application"; + } else if (running) { + msg = "Flow is already in course"; + } else { + return new Pair<>(bridge.getTriggerUrl(), null); + } + } + + } catch (Exception e) { + msg = e.getMessage(); + scriptLogger.error(msg, e); + } + return new Pair<>(null, msg); + + } + + private User extractUser(String userId) { + + UserService userService = CdiUtil.bean(UserService.class); + List matchingUsers = userService.getUsersByAttribute(finishIdAttr, userId, true, 2); + int matches = matchingUsers.size(); + + if (matches != 1) { + if (matches == 0) { + scriptLogger.warn("No user matches the required condition: {}={}", finishIdAttr, userId); + } else { + scriptLogger.warn("Several users match the required condition: {}={}", finishIdAttr, userId); + } + + return null; + } + return matchingUsers.get(0); + + } + + @Override + public boolean authorize(Object scriptContext) { + + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + + if (!CdiUtil.bean(FlowUtils.class).serviceEnabled()) + return makeUnexpectedError(context, null, "Agama engine is disabled"); + + if (!context.getAuthzRequest().isUseAuthorizationChallengeSession()) + return makeMissingParamError(context, "Please set 'use_auth_session=true' in your request"); + + ServletRequest servletRequest = context.getHttpRequest(); + AuthorizationChallengeSession deviceSessionObject = context.getAuthzRequest().getAuthorizationChallengeSessionObject(); + + boolean noSO = deviceSessionObject == null; + scriptLogger.debug("There IS{} device session object", noSO ? " NO" : ""); + + Map deviceSessionObjectAttrs = null; + String sessionId = null, url = null, payload = null; + + if (noSO) { + + String fname = servletRequest.getParameter("flow_name"); + if (fname == null) + return makeMissingParamError(context, "Parameter 'flow_name' missing in request"); + + deviceSessionObject = deviceSessionService.newAuthorizationChallengeSession(); + sessionId = deviceSessionObject.getId(); + + Pair pre = prepareFlow(sessionId, fname); + url = pre.getFirst(); + + if (url == null) return makeUnexpectedError(context, deviceSessionObject, pre.getSecond()); + + deviceSessionObjectAttrs = deviceSessionObject.getAttributes().getAttributes(); + deviceSessionObjectAttrs.put("url", url); + deviceSessionObjectAttrs.put("client_id", servletRequest.getParameter("client_id")); + deviceSessionObjectAttrs.put("acr_values", servletRequest.getParameter("acr_values")); + deviceSessionObjectAttrs.put("scope", servletRequest.getParameter("scope")); + + deviceSessionService.persist(deviceSessionObject); + + } else { + sessionId = deviceSessionObject.getId(); + deviceSessionObjectAttrs = deviceSessionObject.getAttributes().getAttributes(); + String userId = deviceSessionObjectAttrs.get("userId"); + + if (userId != null) { + User user = extractUser(userId); + + if (user == null) + return makeUnexpectedError(context, deviceSessionObject, "Unable to determine identity of user"); + + context.getExecutionContext().setUser(user); + scriptLogger.debug("User {} is authenticated successfully", user.getUserId()); + + entryManager.remove(deviceSessionObject.getDn(), AuthorizationChallengeSession.class); + return true; + } + + url = deviceSessionObjectAttrs.get("url"); + if (url == null) + return makeUnexpectedError(context, deviceSessionObject, "Illegal state - url is missing in device session object"); + + payload = servletRequest.getParameter("data"); + if (payload == null) + return makeMissingParamError(context, "Parameter 'data' missing in request"); + } + + Pair p = miniBrowser.move(sessionId, url, payload); + MiniBrowser.Outcome result = p.getFirst(); + String strRes = result.toString(); + JSONObject jres = p.getSecond(); + + if (result == CLIENT_ERROR || result == ENGINE_ERROR) { + return makeError(context, deviceSessionObject, true, strRes, jres, 500); + + } else if (result == FLOW_PAUSED){ + url = p.getSecond().remove(MiniBrowser.FLOW_PAUSED_URL_KEY).toString(); + deviceSessionObjectAttrs.put("url", url); + deviceSessionService.merge(deviceSessionObject); + + scriptLogger.info("Next url will be {}", url); + return makeError(context, deviceSessionObject, false, strRes, jres, 401); + + } else if (result == FLOW_FINISHED) { + + try { + AgamaPersistenceService aps = CdiUtil.bean(AgamaPersistenceService.class); + FlowStatus fs = aps.getFlowStatus(sessionId); + + if (fs == null) + return makeUnexpectedError(context, deviceSessionObject, "Flow is not running"); + + FlowResult fr = fs.getResult(); + if (fr == null) + return makeUnexpectedError(context, deviceSessionObject, + "The flow finished but the resulting outcome was not found"); + + JSONObject jobj = new JSONObject(fr); + jobj.remove("aborted"); //just to avoid confusions and questions from users + + if (!fr.isSuccess()) { + scriptLogger.info("Flow DID NOT finished successfully"); + return makeError(context, deviceSessionObject, true, strRes, jobj, 401); + } + + String userId = Optional.ofNullable(fr.getData()).map(d -> d.get("userId")) + .map(Object::toString).orElse(null); + + if (userId == null) + return makeUnexpectedError(context, deviceSessionObject, "Unable to determine identity of user. " + + "No userId provided in flow result"); + + deviceSessionObjectAttrs.put("userId", userId); + deviceSessionService.merge(deviceSessionObject); + aps.terminateFlow(sessionId); + + return makeError(context, deviceSessionObject, false, strRes, jobj, 401); + + } catch (IOException e) { + return makeUnexpectedError(context, deviceSessionObject, e.getMessage()); + } + } else { + return makeUnexpectedError(context, deviceSessionObject, "Illegal state - unexpected outcome " + strRes); + } + + } + + @Override + public boolean init(Map configurationAttributes) { + scriptLogger.info("Initialized Agama AuthorizationChallenge Java custom script"); + return true; + } + + @Override + public boolean init(CustomScript customScript, Map configurationAttributes) { + + scriptLogger.info("Initialized Agama AuthorizationChallenge Java custom script."); + finishIdAttr = null; + String name = "finish_userid_db_attribute"; + SimpleCustomProperty prop = configurationAttributes.get(name); + + if (prop != null) { + finishIdAttr = prop.getValue2(); + if (StringHelper.isEmpty(finishIdAttr)) { + finishIdAttr = null; + } + } + + if (finishIdAttr == null) { + scriptLogger.info("Property '{}' is missing value", name); + return false; + } + scriptLogger.info("DB attribute '{}' will be used to map the identity of userId passed "+ + "in Finish directives (if any)", finishIdAttr); + + entryManager = CdiUtil.bean(PersistenceEntryManager.class); + deviceSessionService = CdiUtil.bean(AuthorizationChallengeSessionService.class); + miniBrowser = new MiniBrowser(CdiUtil.bean(AppConfiguration.class).getIssuer()); + return true; + + } + + @Override + public boolean destroy(Map configurationAttributes) { + scriptLogger.info("Destroyed Agama AuthorizationChallenge Java custom script."); + return true; + } + + @Override + public int getApiVersion() { + return 11; + } + + @Override + public Map getAuthenticationMethodClaims(Object context) { + return Map.of(); + } + +} diff --git a/jans-auth-server/agama/engine/pom.xml b/jans-auth-server/agama/engine/pom.xml index 0a73dab51d0..e71e6898375 100644 --- a/jans-auth-server/agama/engine/pom.xml +++ b/jans-auth-server/agama/engine/pom.xml @@ -219,6 +219,15 @@ zip4j 2.11.5 + + + com.nimbusds + oauth2-oidc-sdk + + + org.json + json + @@ -237,11 +246,6 @@ ${project.version} test - - org.json - json - test - org.apache.logging.log4j 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 c334e4ce8f2..439c8987227 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 @@ -58,7 +58,7 @@ public Boolean prepareFlow(String sessionId, String qname, String jsonInput, boo } if (st == null) { - int timeout = aps.getEffectiveFlowTimeout(qname); + int timeout = aps.getEffectiveFlowTimeout(qname, nativeClient); if (timeout <= 0) throw new Exception("Flow timeout negative or zero. " + "Check your AS configuration or flow definition"); long expireAt = System.currentTimeMillis() + 1000L * timeout; diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/client/MiniBrowser.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/client/MiniBrowser.java new file mode 100644 index 00000000000..6859380f3e2 --- /dev/null +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/client/MiniBrowser.java @@ -0,0 +1,211 @@ +package io.jans.agama.engine.client; + +import com.nimbusds.oauth2.sdk.http.HTTPRequest; +import com.nimbusds.oauth2.sdk.http.HTTPResponse; + +import io.jans.util.Pair; + +import jakarta.ws.rs.core.*; +import jakarta.ws.rs.core.Response.Status.Family; +import java.io.IOException; +import java.net.*; +import java.util.*; + +import org.json.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.nimbusds.oauth2.sdk.http.HTTPRequest.Method.*; +import static java.nio.charset.StandardCharsets.UTF_8; + +class WebResponse { + + private int status; + private String body; + private String contentType; + + private WebResponse() { } + + static WebResponse from(HTTPResponse response) { + + WebResponse wr = new WebResponse(); + wr.status = response.getStatusCode(); + wr.contentType = response.getHeaderValue(HttpHeaders.CONTENT_TYPE); + wr.body = response.getBody(); + return wr; + + } + + public int getStatus() { + return status; + } + + public String getBody() { + return body; + } + + public String getContentType() { + return contentType; + } + +} + +/** + * A micro HTTP client capable of interacting with the Agama engine in order to run JSON-based + * flows. It lessens the effort of exchanging messages with the engine and serves as a utility + * to make possible the fact of running Agama flows in native applications.
+ * The 'move' method implements the POST-REDIRECT-GET pattern of the engine: at every step, the + * URL passed is supplied with the given JSON contents, and the HTTP redirect is followed. The + * method returns one of the possible outcomes (see Outcome enum) plus some JSON data.
+ * This is an explanation of outcomes: CLIENT_ERROR (problems to connect to the URL or read the + * response), ENGINE_ERROR (the flow crashed, timed out, or an RFAC instruction was reached), + * FLOW_FINISHED (a Finish instruction was executed), and FLOW_PAUSED (RRF instruction was hit).
+ * The JSON data returned by 'move' contains error data (CLIENT_ERROR or ENGINE_ERROR), the data + * associated to the Finish instruction (FLOW_FINISHED), or the data supplied to the RRF instruction + * (FLOW_PAUSED). Only in the last case the method may receive a subsequent invocation, where the + * JSON data to supply is supposed to emulate the output of the RRF execution, that is, the result + * of having submitted a UI form in the app (desktop or mobile).
+ * Thus, it is the native app that takes charge of the UI rendering by receiving the same data + * the equivalent Freemarker template would receive (in the web world), and the data of the form + * submission is built by the native app too. In this case the path to the UI template (in RRF) + * has no effect, but it is anyways included in the output of 'move' for reference. + */ +public class MiniBrowser { + + public enum Outcome { CLIENT_ERROR, ENGINE_ERROR, FLOW_FINISHED, FLOW_PAUSED } + + public static final String FLOW_PAUSED_URL_KEY = "_url"; + + private static final Logger logger = LoggerFactory.getLogger(MiniBrowser.class); + + private String rootUrl; + private int connectionTimeout; + private int readTimeout; + private int maxErrorContentLength; + + public MiniBrowser(String rootUrl) { + this(rootUrl, 3500, 3500, 4096); + } + + public MiniBrowser(String rootUrl, + int connectionTimeout, int readTimeout, int maxErrorContentLength) { + + this.rootUrl = rootUrl; + this.connectionTimeout = connectionTimeout; + this.readTimeout = readTimeout; + this.maxErrorContentLength = maxErrorContentLength; + + } + + public Pair move(String phantomSid, String relativeUrl, String jsonPayload) { + + try { + return moveImpl(phantomSid, relativeUrl, jsonPayload); + } catch (Exception e) { + String error = e.getMessage(); + logger.error(error, e); + + JSONObject jobj = new JSONObject(Map.of("description", error)); + return new Pair<>(Outcome.CLIENT_ERROR, jobj); + } + + } + + private Pair moveImpl(String phantomSid, String relativeUrl, String jsonPayload) + throws Exception { + + String error = null; + String url = normalize(relativeUrl); + logger.info("Moving forward from {}", url); + + HTTPResponse response = sendRequest(phantomSid, new URL(url), jsonPayload); + WebResponse wr = WebResponse.from(response); + int status = wr.getStatus(); + + if (Family.familyOf(status).equals(Family.REDIRECTION)) { + String location = response.getHeaderValue(HttpHeaders.LOCATION); + + if (location != null) { + wr = null; + logger.info("Redirecting to {}", location); + + response = sendRequest(phantomSid, new URL(normalize(location)), null); + wr = WebResponse.from(response); + + if (MediaType.APPLICATION_JSON.equals(wr.getContentType()) && wr.getStatus() == 200) { + + logger.info("Returning JSON contents"); + JSONObject jobj = new JSONObject(wr.getBody()); + + jobj.put(FLOW_PAUSED_URL_KEY, location); + return new Pair<>(Outcome.FLOW_PAUSED, jobj); + } + + error = "Expecting OK JSON response for " + location; + + } else { + error = "Target of redirection is missing"; + } + } else if (MediaType.APPLICATION_JSON.equals(wr.getContentType()) && status == 200) { + + logger.info("Seems to have landed to the finish page"); + JSONObject jobj = new JSONObject(wr.getBody()); + + if (jobj.has("success")) return new Pair<>(Outcome.FLOW_FINISHED, jobj); + + error = "Unexpected response to " + url; + + } else { + error = "Unexpected response to " + url; + } + + logger.error(error); + JSONObject jobj = new JSONObject(Map.of("description", error)); + + String contentType = wr.getContentType(); + jobj.put("status", wr.getStatus()); + jobj.put("contentType", Optional.ofNullable((Object) contentType).orElse(JSONObject.NULL)); + + String body = wr.getBody(); + if (body == null) { + jobj.put("body", JSONObject.NULL); + } else if (MediaType.APPLICATION_JSON.equals(contentType)) { + jobj.put("body", new JSONObject(body)); + } else { + body = body.substring(0, Math.min(body.length(), maxErrorContentLength)); + jobj.put("body", body); + } + + return new Pair<>(Outcome.ENGINE_ERROR, jobj); + + } + + private HTTPResponse sendRequest(String phantomSid, URL url, String jsonPayload) throws IOException { + + boolean noPayload = jsonPayload == null; + HTTPRequest request = new HTTPRequest(noPayload ? GET : POST, url); + request.setConnectTimeout(connectionTimeout); + request.setReadTimeout(readTimeout); + //Ideally, redirects should be followed, but cookies are lost between requests :( + request.setFollowRedirects(false); + //... and without following redirects, the content-type has to be passed at every request :( + + //the presence of this header signals the engine not to read the incoming data as application/x-www-form-urlencoded + //and also to use the json version of engine's error pages + request.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + //sending the session_id cookie helps maintain the state of the running flow between server/client + request.setHeader​(HttpHeaders.COOKIE, String.format("session_id=%s;", phantomSid)); + + if (!noPayload) { + request.setBody(jsonPayload); + } + return request.send(); + + } + + private String normalize(String relativeUrl) { + String url = relativeUrl.startsWith(rootUrl) ? "" : rootUrl; + return url + relativeUrl; + } + +} 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 89e95632222..659a6de7da5 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 @@ -111,19 +111,25 @@ public boolean flowEnabled(String flowName) { } - public int getEffectiveFlowTimeout(String flowName) { + public int getEffectiveFlowTimeout(String flowName, boolean nativeClient) { Flow fl = entryManager.findEntries(AGAMA_FLOWS_BASE, Flow.class, Filter.createEqualityFilter(Flow.ATTR_NAMES.QNAME, flowName), new String[]{ Flow.ATTR_NAMES.META }, 1).get(0); - int unauth = appConfiguration.getSessionIdUnauthenticatedUnusedLifetime(); + int unauth = appConfiguration.getSessionIdUnauthenticatedUnusedLifetime(); + if (nativeClient) { + unauth = Optional.ofNullable( + appConfiguration.getAuthorizationChallengeSessionLifetimeInSeconds()) + .orElse(unauth); + } + Integer flowTimeout = fl.getMetadata().getTimeout(); int timeout = Optional.ofNullable(flowTimeout).map(Integer::intValue).orElse(unauth); return Math.min(unauth, timeout); } - + public Flow getFlow(String flowName, boolean full) throws IOException { try { diff --git a/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/BaseTest.java b/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/BaseTest.java index acb5ae64f19..c8f799c9f30 100644 --- a/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/BaseTest.java +++ b/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/BaseTest.java @@ -155,11 +155,11 @@ void validateFinishPage(HtmlPage page, boolean success) { } void assertOK(Page page) { - assertEquals(page.getWebResponse().getStatusCode(), WebResponse.OK); + assertEquals(page.getWebResponse().getStatusCode(), 200); } void assertServerError(Page page) { - assertEquals(page.getWebResponse().getStatusCode(), WebResponse.INTERNAL_SERVER_ERROR); + assertEquals(page.getWebResponse().getStatusCode(), 500); } void assertTextContained(String text, String ...words) { diff --git a/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/CustomConfigsFlowTest.java b/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/CustomConfigsFlowTest.java index 83ce021096a..869a91440e0 100644 --- a/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/CustomConfigsFlowTest.java +++ b/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/CustomConfigsFlowTest.java @@ -25,10 +25,10 @@ public void withTimeout() { int status = page.getWebResponse().getStatusCode(); String text = page.getVisibleText().toLowerCase(); - if (status == WebResponse.OK) { + if (status == 200) { //See timeout.ftlh assertTextContained(text, "took", "more", "expected"); - } else if (status == WebResponse.NOT_FOUND) { + } else if (status == 404) { //See mismatch.ftlh assertTextContained(text, "not", "found"); } else { diff --git a/jans-linux-setup/jans_setup/templates/scripts.ldif b/jans-linux-setup/jans_setup/templates/scripts.ldif index 5a4defd2202..a3c926ae360 100644 --- a/jans-linux-setup/jans_setup/templates/scripts.ldif +++ b/jans-linux-setup/jans_setup/templates/scripts.ldif @@ -564,6 +564,21 @@ jansRevision: 1 jansScr::%(authorization_challenge_authorizationchallenge)s jansScrTyp: authorization_challenge +dn: inum=BADA-B000,ou=scripts,o=jans +objectClass: jansCustomScr +objectClass: top +description: Agama Authorization Challenge Script +displayName: agama_challenge +inum: BADA-B000 +jansEnabled: true +jansLevel: 1 +jansModuleProperty: {"value1":"location_type","value2":"db","description":""} +jansConfProperty: {"value1":"finish_userid_db_attribute","value2":"uid","description":""} +jansProgLng: java +jansRevision: 1 +jansScrTyp: authorization_challenge +jansScr::%(authorization_challenge_agamachallenge)s + dn: inum=0300-BB00,ou=scripts,o=jans objectClass: jansCustomScr objectClass: top diff --git a/mkdocs.yml b/mkdocs.yml index 05159ef1d84..ada56fb3a94 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -281,6 +281,7 @@ nav: - Agama Best Practices: janssen-server/developer/agama/agama-best-practices.md - Advanced usages: janssen-server/developer/agama/advanced-usages.md - Engine and bridge configurations: janssen-server/developer/agama/engine-bridge-config.md + - Agama flows in native applications: janssen-server/developer/agama/native-applications.md - FAQ: janssen-server/developer/agama/faq.md - Quick Start Using Agama Lab: janssen-server/developer/agama/quick-start-using-agama-lab.md - External Libraries: janssen-server/developer/external-libraries.md