-
Notifications
You must be signed in to change notification settings - Fork 76
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: jgomer2001 <[email protected]>
- Loading branch information
1 parent
1b248af
commit ccd1f98
Showing
2 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
317 changes: 317 additions & 0 deletions
317
docs/script-catalog/authorization_challenge/AgamaChallenge.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters