Skip to content

Commit

Permalink
feat: add Challenge script #10460
Browse files Browse the repository at this point in the history
Signed-off-by: jgomer2001 <[email protected]>
  • Loading branch information
jgomer2001 committed Jan 10, 2025
1 parent 1b248af commit ccd1f98
Show file tree
Hide file tree
Showing 2 changed files with 332 additions and 0 deletions.
317 changes: 317 additions & 0 deletions docs/script-catalog/authorization_challenge/AgamaChallenge.java
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<User> 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<String, String> 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<String, String> 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<MiniBrowser.Outcome, JSONObject> 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<String, SimpleCustomProperty> configurationAttributes) {
scriptLogger.info("Initialized Agama AuthorizationChallenge Java custom script");
return true;
}

@Override
public boolean init(CustomScript customScript, Map<String, SimpleCustomProperty> 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<String, SimpleCustomProperty> configurationAttributes) {
scriptLogger.info("Destroyed Agama AuthorizationChallenge Java custom script.");
return true;
}

@Override
public int getApiVersion() {
return 11;
}

@Override
public Map<String, String> getAuthenticationMethodClaims(Object context) {
return Map.of();
}

}
15 changes: 15 additions & 0 deletions jans-linux-setup/jans_setup/templates/scripts.ldif
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit ccd1f98

Please sign in to comment.